From 747e255cda0514cfcb76ef5783ff89ccdc57dfb2 Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Mon, 22 Dec 2025 09:45:35 -0800 Subject: [PATCH 1/5] Adding a tool that requires specific Entra group --- infra/auth_init.py | 87 +++++++++++++++++++++++++++++++++++++++++++-- servers/auth_mcp.py | 76 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 2 deletions(-) diff --git a/infra/auth_init.py b/infra/auth_init.py index a4197d5..9c759d7 100644 --- a/infra/auth_init.py +++ b/infra/auth_init.py @@ -3,19 +3,26 @@ import random import subprocess import uuid +from dataclasses import dataclass from azure.identity.aio import AzureDeveloperCliCredential from dotenv_azd import load_azd_env +from kiota_abstractions.api_error import APIError +from kiota_abstractions.base_request_configuration import RequestConfiguration +from msgraph import GraphServiceClient from msgraph.generated.applications.item.add_password.add_password_post_request_body import ( AddPasswordPostRequestBody, ) from msgraph.generated.models.api_application import ApiApplication from msgraph.generated.models.application import Application +from msgraph.generated.models.o_auth2_permission_grant import OAuth2PermissionGrant from msgraph.generated.models.password_credential import PasswordCredential from msgraph.generated.models.permission_scope import PermissionScope from msgraph.generated.models.service_principal import ServicePrincipal from msgraph.generated.models.web_application import WebApplication -from msgraph.graph_service_client import GraphServiceClient +from msgraph.generated.oauth2_permission_grants.oauth2_permission_grants_request_builder import ( + Oauth2PermissionGrantsRequestBuilder, +) async def get_application(graph_client: GraphServiceClient, app_id: str) -> str | None: @@ -171,6 +178,79 @@ async def create_or_update_fastmcp_app(graph_client: GraphServiceClient) -> None update_azd_env(app_secret_env_var, client_secret) print("Client secret created and saved to environment.") + return object_id, app_id + + +@dataclass +class GrantDefinition: + principal_id: str + resource_app_id: str + scopes: list[str] + target_label: str + + def scope_string(self) -> str: + return " ".join(self.scopes) + + +async def grant_application_admin_consent(graph_client: GraphServiceClient, server_app_id: str): + server_principal = await graph_client.service_principals_with_app_id(server_app_id).get() + if server_principal is None or server_principal.id is None: + raise ValueError("Unable to locate service principal for server application") + + grant_definitions = [ + GrantDefinition( + principal_id=server_principal.id, + resource_app_id="00000003-0000-0000-c000-000000000000", + scopes=["User.Read", "email", "offline_access", "openid", "profile"], + target_label="server application", + ), + GrantDefinition( + principal_id=server_principal.id, + resource_app_id="880da380-985e-4198-81b9-e05b1cc53158", + scopes=["user_impersonation"], + target_label="server application", + ), + ] + + for grant in grant_definitions: + resource_principal = await graph_client.service_principals_with_app_id(grant.resource_app_id).get() + if resource_principal is None or resource_principal.id is None: + raise ValueError(f"Unable to locate service principal for resource {grant.resource_app_id}") + + desired_scope = grant.scope_string() + filter_query = f"clientId eq '{grant.principal_id}' and resourceId eq '{resource_principal.id}'" + query_params = Oauth2PermissionGrantsRequestBuilder.Oauth2PermissionGrantsRequestBuilderGetQueryParameters( + filter=filter_query + ) + request_config = RequestConfiguration[ + Oauth2PermissionGrantsRequestBuilder.Oauth2PermissionGrantsRequestBuilderGetQueryParameters + ](query_parameters=query_params) + existing_grants = await graph_client.oauth2_permission_grants.get(request_configuration=request_config) + + current_grant = existing_grants.value[0] if existing_grants and existing_grants.value else None + + if current_grant: + print(f"Admin consent already granted for {desired_scope} on the {grant.target_label}") + continue + + try: + await graph_client.oauth2_permission_grants.post( + OAuth2PermissionGrant( + client_id=grant.principal_id, + consent_type="AllPrincipals", + resource_id=resource_principal.id, + scope=desired_scope, + ) + ) + print(f"Granted admin consent for {desired_scope} on the {grant.target_label}") + except APIError as error: + status_code = error.response_status_code + if status_code in {401, 403}: + print(f"Failed to grant admin consent: {error.message}") + return + else: + raise + async def main(): # Configuration - customize these as needed @@ -182,9 +262,12 @@ async def main(): scopes = ["https://graph.microsoft.com/.default"] graph_client = GraphServiceClient(credentials=credential, scopes=scopes) - await create_or_update_fastmcp_app(graph_client) + server_object_id, server_app_id = await create_or_update_fastmcp_app(graph_client) print("Setup complete!") + print("Attempting to grant admin consent for the client and server applications...") + await grant_application_admin_consent(graph_client, server_app_id) + if __name__ == "__main__": load_azd_env() diff --git a/servers/auth_mcp.py b/servers/auth_mcp.py index fc58ae3..7a5f2fe 100644 --- a/servers/auth_mcp.py +++ b/servers/auth_mcp.py @@ -7,6 +7,7 @@ from enum import Enum from typing import Annotated +import httpx import logfire from azure.core.settings import settings from azure.cosmos.aio import CosmosClient @@ -20,6 +21,7 @@ from fastmcp.server.middleware import Middleware, MiddlewareContext from key_value.aio.stores.memory import MemoryStore from keycloak_provider import KeycloakAuthProvider +from msal import ConfidentialClientApplication, TokenCache from opentelemetry.instrumentation.starlette import StarletteInstrumentor from rich.console import Console from rich.logging import RichHandler @@ -124,6 +126,31 @@ logger.error("No authentication configured for MCP server, exiting.") raise SystemExit(1) +confidential_client = ConfidentialClientApplication( + client_id=os.environ["ENTRA_PROXY_AZURE_CLIENT_ID"], + client_credential=os.environ["ENTRA_PROXY_AZURE_CLIENT_SECRET"], + authority=f"https://login.microsoftonline.com/{os.environ['AZURE_TENANT_ID']}", + token_cache=TokenCache(), +) + + +async def get_user_groups_from_graph(graph_token: str) -> list[dict]: + """Fetch the authenticated user's group memberships from Microsoft Graph API.""" + async with httpx.AsyncClient() as client: + response = await client.get( + "https://graph.microsoft.com/v1.0/me/memberOf", + headers={"Authorization": f"Bearer {graph_token}"}, + ) + response.raise_for_status() + data = response.json() + # Filter to only group objects and extract relevant fields + groups = [ + {"id": item["id"], "displayName": item.get("displayName")} + for item in data.get("value", []) + if item.get("@odata.type") == "#microsoft.graph.group" + ] + return groups + # Middleware to populate user_id in per-request context state class UserAuthMiddleware(Middleware): @@ -242,6 +269,55 @@ async def get_user_expenses(ctx: Context): return f"Error: Unable to retrieve expense data - {str(e)}" +@mcp.tool +async def get_expense_stats(ctx: Context): + """Get a statistical summary of expenses (count per category) for all users. + Only accessible to users in the authorized admin group. + """ + access_token = get_access_token() + if not access_token: + return "Error: Authentication required" + + auth_token = access_token.token + try: + graph_resource_access_token = confidential_client.acquire_token_on_behalf_of( + user_assertion=auth_token, scopes=["https://graph.microsoft.com/.default"] + ) + if "error" in graph_resource_access_token: + return "Error: Unable to obtain Graph API access token for authorization check" + + graph_auth_token = graph_resource_access_token["access_token"] + user_groups = await get_user_groups_from_graph(graph_auth_token) + + # Check for the specific admin group ID + admin_group_id = "112345c1-f7b8-4900-bcc6-8e8208bcf560" + is_admin = any(group["id"] == admin_group_id for group in user_groups) + + if not is_admin: + return "Error: Unauthorized. You do not have permission to access expense statistics." + + # Query Cosmos DB for stats across all users + # We fetch categories and aggregate in Python to avoid cross-partition GROUP BY limitations + query = "SELECT c.category FROM c" + stats = {} + async for item in cosmos_container.query_items(query=query): + category = item.get("category", "Unknown") + stats[category] = stats.get(category, 0) + 1 + + if not stats: + return "No expense data found to summarize." + + summary = "Expense Statistics (Count per Category):\n" + for category, count in stats.items(): + summary += f"- Category {category}: {count} expenses\n" + + return summary + + except Exception as e: + logger.error(f"Error retrieving expense stats: {str(e)}") + return f"Error: Unable to retrieve expense statistics - {str(e)}" + + @mcp.custom_route("/health", methods=["GET"]) async def health_check(_request): """Health check endpoint for service availability.""" From 3fe12b87578562de2ff2b72d495126d37568c6df Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Mon, 22 Dec 2025 10:07:43 -0800 Subject: [PATCH 2/5] Address feedback, dont request user_impersonation --- infra/auth_init.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/infra/auth_init.py b/infra/auth_init.py index 9c759d7..229edfa 100644 --- a/infra/auth_init.py +++ b/infra/auth_init.py @@ -143,7 +143,7 @@ def update_app_with_identifier_uri(client_id: str) -> Application: ) -async def create_or_update_fastmcp_app(graph_client: GraphServiceClient) -> None: +async def create_or_update_fastmcp_app(graph_client: GraphServiceClient): """Create or update a FastMCP app registration.""" app_id_env_var = "ENTRA_PROXY_AZURE_CLIENT_ID" app_secret_env_var = "ENTRA_PROXY_AZURE_CLIENT_SECRET" @@ -178,7 +178,7 @@ async def create_or_update_fastmcp_app(graph_client: GraphServiceClient) -> None update_azd_env(app_secret_env_var, client_secret) print("Client secret created and saved to environment.") - return object_id, app_id + return app_id @dataclass @@ -203,13 +203,7 @@ async def grant_application_admin_consent(graph_client: GraphServiceClient, serv resource_app_id="00000003-0000-0000-c000-000000000000", scopes=["User.Read", "email", "offline_access", "openid", "profile"], target_label="server application", - ), - GrantDefinition( - principal_id=server_principal.id, - resource_app_id="880da380-985e-4198-81b9-e05b1cc53158", - scopes=["user_impersonation"], - target_label="server application", - ), + ) ] for grant in grant_definitions: @@ -262,12 +256,13 @@ async def main(): scopes = ["https://graph.microsoft.com/.default"] graph_client = GraphServiceClient(credentials=credential, scopes=scopes) - server_object_id, server_app_id = await create_or_update_fastmcp_app(graph_client) - print("Setup complete!") + server_app_id = await create_or_update_fastmcp_app(graph_client) print("Attempting to grant admin consent for the client and server applications...") await grant_application_admin_consent(graph_client, server_app_id) + print("✅ Entra app registration setup is complete.") + if __name__ == "__main__": load_azd_env() From 95cf235486cf8e88cb9076c6745be3ac22f5f660 Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Mon, 22 Dec 2025 10:49:56 -0800 Subject: [PATCH 3/5] Use an azd env var for the group ID, and use transitiveMemberOf instead --- infra/main.bicep | 4 ++++ infra/main.parameters.json | 3 +++ infra/server.bicep | 5 +++++ infra/write_env.ps1 | 1 + infra/write_env.sh | 1 + servers/auth_mcp.py | 32 +++++++++++++++++--------------- 6 files changed, 31 insertions(+), 15 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 70e2d6a..5b569d3 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -78,6 +78,9 @@ param keycloakMcpServerAudience string = 'mcp-server' @description('Flag to restrict ACR public network access (requires VPN for local image push when true)') param usePrivateAcr bool = false +@description('Entra ID group ID for admin access to expense statistics (only used when mcpAuthProvider is entra_proxy)') +param entraAdminGroupId string = '' + @description('Flag to restrict Log Analytics public query access for increased security') param usePrivateLogAnalytics bool = false @@ -790,6 +793,7 @@ module server 'server.bicep' = { entraProxyClientSecret: useEntraProxy ? entraProxyClientSecret : '' entraProxyBaseUrl: useEntraProxy ? entraProxyMcpServerBaseUrl : '' tenantId: useEntraProxy ? tenant().tenantId : '' + entraAdminGroupId: useEntraProxy ? entraAdminGroupId : '' mcpAuthProvider: mcpAuthProvider logfireToken: logfireToken } diff --git a/infra/main.parameters.json b/infra/main.parameters.json index e7d274f..f039a9a 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -56,6 +56,9 @@ "entraProxyClientSecret": { "value": "${ENTRA_PROXY_AZURE_CLIENT_SECRET}" }, + "entraAdminGroupId": { + "value": "${ENTRA_ADMIN_GROUP_ID}" + }, "logfireToken": { "value": "${LOGFIRE_TOKEN}" } diff --git a/infra/server.bicep b/infra/server.bicep index bc8084a..c5afc15 100644 --- a/infra/server.bicep +++ b/infra/server.bicep @@ -29,6 +29,7 @@ param entraProxyClientId string = '' param entraProxyClientSecret string = '' param entraProxyBaseUrl string = '' param tenantId string = '' +param entraAdminGroupId string = '' @secure() param logfireToken string = '' @allowed([ @@ -139,6 +140,10 @@ var entraProxyEnv = !empty(entraProxyClientId) ? [ name: 'AZURE_TENANT_ID' value: tenantId } + { + name: 'ENTRA_ADMIN_GROUP_ID' + value: entraAdminGroupId + } ] : [] // Secrets for sensitive values diff --git a/infra/write_env.ps1 b/infra/write_env.ps1 index da228d1..cf5c4ed 100644 --- a/infra/write_env.ps1 +++ b/infra/write_env.ps1 @@ -58,6 +58,7 @@ if ($ENTRA_PROXY_AZURE_CLIENT_ID -and $ENTRA_PROXY_AZURE_CLIENT_ID -ne "") { Add-Content -Path $ENV_FILE_PATH -Value "ENTRA_PROXY_AZURE_CLIENT_ID=$ENTRA_PROXY_AZURE_CLIENT_ID" Write-Env ENTRA_PROXY_AZURE_CLIENT_SECRET Write-Env ENTRA_PROXY_MCP_SERVER_BASE_URL + Write-EnvIfSet ENTRA_ADMIN_GROUP_ID } Add-Content -Path $ENV_FILE_PATH -Value "MCP_ENTRY=$(azd env get-value MCP_ENTRY)" Add-Content -Path $ENV_FILE_PATH -Value "MCP_SERVER_URL=$(azd env get-value MCP_SERVER_URL)" diff --git a/infra/write_env.sh b/infra/write_env.sh index 618e02d..2c37b45 100755 --- a/infra/write_env.sh +++ b/infra/write_env.sh @@ -64,6 +64,7 @@ if [ -n "$ENTRA_PROXY_AZURE_CLIENT_ID" ]; then echo "ENTRA_PROXY_AZURE_CLIENT_ID=${ENTRA_PROXY_AZURE_CLIENT_ID}" >> "$ENV_FILE_PATH" write_env ENTRA_PROXY_AZURE_CLIENT_SECRET write_env ENTRA_PROXY_MCP_SERVER_BASE_URL + write_env_if_set ENTRA_ADMIN_GROUP_ID fi echo "MCP_ENTRY=$(azd env get-value MCP_ENTRY)" >> "$ENV_FILE_PATH" echo "MCP_SERVER_URL=$(azd env get-value MCP_SERVER_URL)" >> "$ENV_FILE_PATH" diff --git a/servers/auth_mcp.py b/servers/auth_mcp.py index 7a5f2fe..585f453 100644 --- a/servers/auth_mcp.py +++ b/servers/auth_mcp.py @@ -134,22 +134,23 @@ ) -async def get_user_groups_from_graph(graph_token: str) -> list[dict]: - """Fetch the authenticated user's group memberships from Microsoft Graph API.""" +async def check_user_in_group(graph_token: str, group_id: str) -> bool: + """Check if the authenticated user is a member of the specified group (including transitive membership).""" async with httpx.AsyncClient() as client: + url = ( + "https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group" + f"?$filter=id eq '{group_id}'&$count=true" + ) response = await client.get( - "https://graph.microsoft.com/v1.0/me/memberOf", - headers={"Authorization": f"Bearer {graph_token}"}, + url, + headers={ + "Authorization": f"Bearer {graph_token}", + "ConsistencyLevel": "eventual", + }, ) response.raise_for_status() data = response.json() - # Filter to only group objects and extract relevant fields - groups = [ - {"id": item["id"], "displayName": item.get("displayName")} - for item in data.get("value", []) - if item.get("@odata.type") == "#microsoft.graph.group" - ] - return groups + return data.get("@odata.count", 0) > 0 # Middleware to populate user_id in per-request context state @@ -287,11 +288,12 @@ async def get_expense_stats(ctx: Context): return "Error: Unable to obtain Graph API access token for authorization check" graph_auth_token = graph_resource_access_token["access_token"] - user_groups = await get_user_groups_from_graph(graph_auth_token) - # Check for the specific admin group ID - admin_group_id = "112345c1-f7b8-4900-bcc6-8e8208bcf560" - is_admin = any(group["id"] == admin_group_id for group in user_groups) + # Check for the specific admin group ID using transitive membership + admin_group_id = os.environ.get("ENTRA_ADMIN_GROUP_ID", "") + if not admin_group_id: + return "Error: Admin group ID not configured. Set ENTRA_ADMIN_GROUP_ID environment variable." + is_admin = await check_user_in_group(graph_auth_token, admin_group_id) if not is_admin: return "Error: Unauthorized. You do not have permission to access expense statistics." From 503bf19c72cc531133efad9856a1e917e60bcd6a Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Wed, 7 Jan 2026 14:21:52 -0800 Subject: [PATCH 4/5] Fork auth files into entra vs keycloak --- AGENTS.md | 8 + README.md | 2 +- infra/server.bicep | 9 +- pyproject.toml | 8 +- servers/Dockerfile | 2 +- servers/{auth_mcp.py => auth_entra_mcp.py} | 89 ++--- servers/auth_keycloak_mcp.py | 223 ++++++++++++ servers/entrypoint.sh | 7 +- spanish/README.md | 2 +- uv.lock | 401 ++++++++++++++++----- 10 files changed, 591 insertions(+), 160 deletions(-) rename servers/{auth_mcp.py => auth_entra_mcp.py} (80%) create mode 100644 servers/auth_keycloak_mcp.py diff --git a/AGENTS.md b/AGENTS.md index 7831d5f..6ec59b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,3 +29,11 @@ When adding new azd environment variables, update these files: 6. **infra/main.bicep outputs**: If the value needs to be stored back in azd env after provisioning: - Add an output (note: `@secure()` parameters cannot be outputs) + +## Updating Python dependencies + +When updating or adding Python dependencies: + +1. Edit `pyproject.toml` with the new or updated version constraints. +2. Run `uv lock` to re-resolve dependencies (use `uv lock -P ` to upgrade only a specific package). +3. Run `uv sync` to install the updated lockfile into the virtual environment. diff --git a/README.md b/README.md index 8a75809..fa26a23 100644 --- a/README.md +++ b/README.md @@ -547,7 +547,7 @@ After deployment, you can test locally with OAuth enabled: ```bash # Run the MCP server -cd servers && uvicorn auth_mcp:app --host 0.0.0.0 --port 8000 +cd servers && uvicorn auth_entra_mcp:app --host 0.0.0.0 --port 8000 ``` The server will use the Entra App Registration for OAuth and CosmosDB for client storage. diff --git a/infra/server.bicep b/infra/server.bicep index c5afc15..1efd6a0 100644 --- a/infra/server.bicep +++ b/infra/server.bicep @@ -40,8 +40,13 @@ param logfireToken string = '' param mcpAuthProvider string = 'none' // Base environment variables -// Select MCP entrypoint based on configured auth (Keycloak or FastMCP Azure auth) -var mcpEntry = (!empty(keycloakRealmUrl) || !empty(entraProxyClientId)) ? 'auth' : 'deployed' +// Select MCP entrypoint based on configured auth +// - Keycloak → 'auth_keycloak_mcp' +// - Entra OAuth Proxy → 'auth_entra_mcp' +// - None → 'deployed_mcp' +var mcpEntry = mcpAuthProvider == 'keycloak' + ? 'auth_keycloak_mcp' + : (mcpAuthProvider == 'entra_proxy' ? 'auth_entra_mcp' : 'deployed_mcp') var baseEnv = [ { name: 'AZURE_OPENAI_CHAT_DEPLOYMENT' diff --git a/pyproject.toml b/pyproject.toml index dc14aca..104c67c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Demonstration of Python FastMCP servers" readme = "README.md" requires-python = "==3.13.*" dependencies = [ - "fastmcp>=2.13.3", + "fastmcp>=2.14.2", "debugpy>=1.8.0", "langchain-core>=0.3.0", "mcp>=1.3.0", @@ -18,9 +18,9 @@ dependencies = [ "azure-ai-agents>=1.1.0", "agent-framework>=1.0.0b251016", "azure-cosmos>=4.9.0", - "azure-monitor-opentelemetry>=1.6.4", - "opentelemetry-instrumentation-starlette>=0.49b0", - "opentelemetry-exporter-otlp-proto-grpc>=1.28.0", + "azure-monitor-opentelemetry>=1.8.3", + "opentelemetry-instrumentation-starlette>=0.60b0", + "opentelemetry-exporter-otlp-proto-grpc>=1.39.0", "logfire>=4.15.1", "azure-core-tracing-opentelemetry>=1.0.0b12" ] diff --git a/servers/Dockerfile b/servers/Dockerfile index 253f1bd..c522b16 100644 --- a/servers/Dockerfile +++ b/servers/Dockerfile @@ -37,7 +37,7 @@ ENV PATH="/code/.venv/bin:$PATH" EXPOSE 8000 -ENV MCP_ENTRY=deployed +ENV MCP_ENTRY=deployed_mcp # Copy and use entrypoint script for clean env expansion COPY --chown=app:app servers/entrypoint.sh /usr/local/bin/entrypoint.sh diff --git a/servers/auth_mcp.py b/servers/auth_entra_mcp.py similarity index 80% rename from servers/auth_mcp.py rename to servers/auth_entra_mcp.py index 585f453..3fc838c 100644 --- a/servers/auth_mcp.py +++ b/servers/auth_entra_mcp.py @@ -1,8 +1,13 @@ -"""Run with: cd servers && uvicorn auth_mcp:app --host 0.0.0.0 --port 8000""" +""" +Expense tracking MCP server with Entra authentication and Cosmos DB storage. + +Run with: cd servers && uvicorn auth_entra_mcp:app --host 0.0.0.0 --port 8000 +""" import logging import os import uuid +import warnings from datetime import date from enum import Enum from typing import Annotated @@ -20,7 +25,6 @@ from fastmcp.server.dependencies import get_access_token from fastmcp.server.middleware import Middleware, MiddlewareContext from key_value.aio.stores.memory import MemoryStore -from keycloak_provider import KeycloakAuthProvider from msal import ConfidentialClientApplication, TokenCache from opentelemetry.instrumentation.starlette import StarletteInstrumentor from rich.console import Console @@ -36,7 +40,7 @@ logging.basicConfig( level=logging.WARNING, - format="%(message)s", + format="%(name)s: %(message)s", handlers=[ RichHandler( console=Console(stderr=True), @@ -46,6 +50,9 @@ ) ], ) +# Suppress OTEL 1.39 deprecation warnings and noisy logs +warnings.filterwarnings("ignore", category=DeprecationWarning, message=r".*Deprecated since version 1\.39\.0.*") +logging.getLogger("azure.monitor.opentelemetry.exporter._performance_counters._manager").setLevel(logging.ERROR) logger = logging.getLogger("ExpensesMCP") logger.setLevel(logging.INFO) @@ -76,55 +83,25 @@ cosmos_container = cosmos_db.get_container_client(os.environ["AZURE_COSMOSDB_USER_CONTAINER"]) # Configure authentication provider -auth = None -mcp_auth_provider = os.getenv("MCP_AUTH_PROVIDER", "none").lower() -if mcp_auth_provider == "entra_proxy": - # Azure/Entra ID authentication using AzureProvider - # When running locally, always use localhost for base URL (OAuth redirects need to match) - oauth_client_store = None - if RUNNING_IN_PRODUCTION: - oauth_container = cosmos_db.get_container_client(os.environ["AZURE_COSMOSDB_OAUTH_CONTAINER"]) - oauth_client_store = CosmosDBStore(container=oauth_container, default_collection="oauth-clients") - entra_base_url = os.environ["ENTRA_PROXY_MCP_SERVER_BASE_URL"] - else: - oauth_client_store = MemoryStore() - entra_base_url = "http://localhost:8000" - auth = AzureProvider( - client_id=os.environ["ENTRA_PROXY_AZURE_CLIENT_ID"], - client_secret=os.environ["ENTRA_PROXY_AZURE_CLIENT_SECRET"], - tenant_id=os.environ["AZURE_TENANT_ID"], - base_url=entra_base_url, - required_scopes=["mcp-access"], - client_storage=oauth_client_store, - ) - logger.info( - "Using Entra OAuth Proxy for server %s and %s storage", entra_base_url, type(oauth_client_store).__name__ - ) -elif mcp_auth_provider == "keycloak": - # Keycloak authentication using KeycloakAuthProvider with DCR support - KEYCLOAK_REALM_URL = os.environ["KEYCLOAK_REALM_URL"] - if RUNNING_IN_PRODUCTION: - keycloak_base_url = os.environ["KEYCLOAK_MCP_SERVER_BASE_URL"] - else: - keycloak_base_url = "http://localhost:8000" - - keycloak_audience = os.getenv("KEYCLOAK_MCP_SERVER_AUDIENCE") or "mcp-server" - - auth = KeycloakAuthProvider( - realm_url=KEYCLOAK_REALM_URL, - base_url=keycloak_base_url, - required_scopes=["openid", "mcp:access"], - audience=keycloak_audience, - ) - logger.info( - "Using Keycloak DCR auth for server %s and realm %s (audience=%s)", - keycloak_base_url, - KEYCLOAK_REALM_URL, - keycloak_audience, - ) +# Azure/Entra ID authentication using AzureProvider +# When running locally, always use localhost for base URL (OAuth redirects need to match) +oauth_client_store = None +if RUNNING_IN_PRODUCTION: + oauth_container = cosmos_db.get_container_client(os.environ["AZURE_COSMOSDB_OAUTH_CONTAINER"]) + oauth_client_store = CosmosDBStore(container=oauth_container, default_collection="oauth-clients") + entra_base_url = os.environ["ENTRA_PROXY_MCP_SERVER_BASE_URL"] else: - logger.error("No authentication configured for MCP server, exiting.") - raise SystemExit(1) + oauth_client_store = MemoryStore() + entra_base_url = "http://localhost:8000" +auth = AzureProvider( + client_id=os.environ["ENTRA_PROXY_AZURE_CLIENT_ID"], + client_secret=os.environ["ENTRA_PROXY_AZURE_CLIENT_SECRET"], + tenant_id=os.environ["AZURE_TENANT_ID"], + base_url=entra_base_url, + required_scopes=["mcp-access"], + client_storage=oauth_client_store, +) +logger.info("Using Entra OAuth Proxy for server %s and %s storage", entra_base_url, type(oauth_client_store).__name__) confidential_client = ConfidentialClientApplication( client_id=os.environ["ENTRA_PROXY_AZURE_CLIENT_ID"], @@ -141,6 +118,7 @@ async def check_user_in_group(graph_token: str, group_id: str) -> bool: "https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group" f"?$filter=id eq '{group_id}'&$count=true" ) + logger.info(f"Checking group membership for group ID: {group_id}") response = await client.get( url, headers={ @@ -150,7 +128,9 @@ async def check_user_in_group(graph_token: str, group_id: str) -> bool: ) response.raise_for_status() data = response.json() - return data.get("@odata.count", 0) > 0 + membership_count = data.get("@odata.count", 0) + logger.info(f"User membership count in group {group_id}: {membership_count}") + return membership_count > 0 # Middleware to populate user_id in per-request context state @@ -159,8 +139,7 @@ def _get_user_id(self): token = get_access_token() if not (token and hasattr(token, "claims")): return None - # Return 'oid' claim if present (for Entra), otherwise fallback to 'sub' (for KeyCloak) - return token.claims.get("oid", token.claims.get("sub")) + return token.claims.get("oid") async def on_call_tool(self, context: MiddlewareContext, call_next): user_id = self._get_user_id() @@ -178,8 +157,6 @@ async def on_read_resource(self, context: MiddlewareContext, call_next): # Create the MCP server mcp = FastMCP("Expenses Tracker", auth=auth, middleware=[OpenTelemetryMiddleware("ExpensesMCP"), UserAuthMiddleware()]) -"""Expense tracking MCP server with authentication and Cosmos DB storage.""" - class PaymentMethod(Enum): AMEX = "amex" diff --git a/servers/auth_keycloak_mcp.py b/servers/auth_keycloak_mcp.py new file mode 100644 index 0000000..bd871e5 --- /dev/null +++ b/servers/auth_keycloak_mcp.py @@ -0,0 +1,223 @@ +""" +Expense tracking MCP server with KeyCloak authentication and Cosmos DB storage. + +Run with: cd servers && uvicorn auth_keycloak_mcp:app --host 0.0.0.0 --port 8000 +""" + +import logging +import os +import uuid +from datetime import date +from enum import Enum +from typing import Annotated + +import logfire +from azure.core.settings import settings +from azure.cosmos.aio import CosmosClient +from azure.identity.aio import DefaultAzureCredential, ManagedIdentityCredential +from azure.monitor.opentelemetry import configure_azure_monitor +from dotenv import load_dotenv +from fastmcp import Context, FastMCP +from fastmcp.server.dependencies import get_access_token +from fastmcp.server.middleware import Middleware, MiddlewareContext +from keycloak_provider import KeycloakAuthProvider +from opentelemetry.instrumentation.starlette import StarletteInstrumentor +from rich.console import Console +from rich.logging import RichHandler +from starlette.responses import JSONResponse + +from opentelemetry_middleware import OpenTelemetryMiddleware + +RUNNING_IN_PRODUCTION = os.getenv("RUNNING_IN_PRODUCTION", "false").lower() == "true" + +if not RUNNING_IN_PRODUCTION: + load_dotenv(override=True) + +logging.basicConfig( + level=logging.WARNING, + format="%(message)s", + handlers=[ + RichHandler( + console=Console(stderr=True), + show_path=False, + show_level=False, + rich_tracebacks=True, + ) + ], +) +logger = logging.getLogger("ExpensesMCP") +logger.setLevel(logging.INFO) + +# Configure Azure SDK OpenTelemetry to use OTEL +settings.tracing_implementation = "opentelemetry" + +# Configure OpenTelemetry exporters based on OPENTELEMETRY_PLATFORM env var +opentelemetry_platform = os.getenv("OPENTELEMETRY_PLATFORM", "none").lower() +if opentelemetry_platform == "appinsights" and os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"): + logger.info("Setting up Azure Monitor instrumentation") + configure_azure_monitor() +elif opentelemetry_platform == "logfire" and os.getenv("LOGFIRE_TOKEN"): + logger.info("Setting up Logfire instrumentation") + logfire.configure(service_name="expenses-mcp", send_to_logfire=True) + +# Configure Cosmos DB client +if RUNNING_IN_PRODUCTION: + azure_credential = ManagedIdentityCredential(client_id=os.environ["AZURE_CLIENT_ID"]) + logger.info("Using Managed Identity Credential for Azure authentication") +else: + azure_credential = DefaultAzureCredential() + logger.info("Using Default Azure Credential for Azure authentication") +cosmos_client = CosmosClient( + url=f"https://{os.environ['AZURE_COSMOSDB_ACCOUNT']}.documents.azure.com:443/", + credential=azure_credential, +) +cosmos_db = cosmos_client.get_database_client(os.environ["AZURE_COSMOSDB_DATABASE"]) +cosmos_container = cosmos_db.get_container_client(os.environ["AZURE_COSMOSDB_USER_CONTAINER"]) + +# Configure Keycloak authentication using KeycloakAuthProvider with DCR support +KEYCLOAK_REALM_URL = os.environ["KEYCLOAK_REALM_URL"] +if RUNNING_IN_PRODUCTION: + keycloak_base_url = os.environ["KEYCLOAK_MCP_SERVER_BASE_URL"] +else: + keycloak_base_url = "http://localhost:8000" + +keycloak_audience = os.getenv("KEYCLOAK_MCP_SERVER_AUDIENCE") or "mcp-server" + +auth = KeycloakAuthProvider( + realm_url=KEYCLOAK_REALM_URL, + base_url=keycloak_base_url, + required_scopes=["openid", "mcp:access"], + audience=keycloak_audience, +) +logger.info( + "Using Keycloak DCR auth for server %s and realm %s (audience=%s)", + keycloak_base_url, + KEYCLOAK_REALM_URL, + keycloak_audience, +) + + +# Middleware to populate user_id in per-request context state +class UserAuthMiddleware(Middleware): + def _get_user_id(self): + token = get_access_token() + if not (token and hasattr(token, "claims")): + return None + return token.claims.get("sub") + + async def on_call_tool(self, context: MiddlewareContext, call_next): + user_id = self._get_user_id() + if context.fastmcp_context is not None: + context.fastmcp_context.set_state("user_id", user_id) + return await call_next(context) + + async def on_read_resource(self, context: MiddlewareContext, call_next): + user_id = self._get_user_id() + if context.fastmcp_context is not None: + context.fastmcp_context.set_state("user_id", user_id) + return await call_next(context) + + +# Create the MCP server +mcp = FastMCP("Expenses Tracker", auth=auth, middleware=[OpenTelemetryMiddleware("ExpensesMCP"), UserAuthMiddleware()]) + + +class PaymentMethod(Enum): + AMEX = "amex" + VISA = "visa" + CASH = "cash" + + +class Category(Enum): + FOOD = "food" + TRANSPORT = "transport" + ENTERTAINMENT = "entertainment" + SHOPPING = "shopping" + GADGET = "gadget" + OTHER = "other" + + +@mcp.tool +async def add_user_expense( + date: Annotated[date, "Date of the expense in YYYY-MM-DD format"], + amount: Annotated[float, "Positive numeric amount of the expense"], + category: Annotated[Category, "Category label"], + description: Annotated[str, "Human-readable description of the expense"], + payment_method: Annotated[PaymentMethod, "Payment method used"], + ctx: Context, +): + """Add a new expense to Cosmos DB.""" + if amount <= 0: + return "Error: Amount must be positive" + + date_iso = date.isoformat() + logger.info(f"Adding expense: ${amount} for {description} on {date_iso}") + + try: + # Read user_id stored by middleware + user_id = ctx.get_state("user_id") + if not user_id: + return "Error: Authentication required (no user_id present)" + expense_id = str(uuid.uuid4()) + expense_item = { + "id": expense_id, + "user_id": user_id, + "date": date_iso, + "amount": amount, + "category": category.value, + "description": description, + "payment_method": payment_method.value, + } + await cosmos_container.create_item(body=expense_item) + return f"Successfully added expense: ${amount} for {description} on {date_iso}" + + except Exception as e: + logger.error(f"Error adding expense: {str(e)}") + return f"Error: Unable to add expense - {str(e)}" + + +@mcp.tool +async def get_user_expenses(ctx: Context): + """Get the authenticated user's expense data from Cosmos DB.""" + + try: + user_id = ctx.get_state("user_id") + if not user_id: + return "Error: Authentication required (no user_id present)" + query = "SELECT * FROM c WHERE c.user_id = @uid ORDER BY c.date DESC" + parameters = [{"name": "@uid", "value": user_id}] + expenses_data = [] + + async for item in cosmos_container.query_items(query=query, parameters=parameters, partition_key=user_id): + expenses_data.append(item) + + if not expenses_data: + return "No expenses found." + + expense_summary = f"Expense data ({len(expenses_data)} entries):\n\n" + for expense in expenses_data: + expense_summary += ( + f"Date: {expense.get('date', 'N/A')}, " + f"Amount: ${expense.get('amount', 0)}, " + f"Category: {expense.get('category', 'N/A')}, " + f"Description: {expense.get('description', 'N/A')}, " + f"Payment: {expense.get('payment_method', 'N/A')}\n" + ) + + return expense_summary + + except Exception as e: + logger.error(f"Error reading expenses: {str(e)}") + return f"Error: Unable to retrieve expense data - {str(e)}" + + +@mcp.custom_route("/health", methods=["GET"]) +async def health_check(_request): + """Health check endpoint for service availability.""" + return JSONResponse({"status": "healthy", "service": "mcp-server"}) + + +# Configure Starlette middleware for OpenTelemetry +# We must do this *after* defining all the MCP server routes +app = mcp.http_app() +StarletteInstrumentor.instrument_app(app) diff --git a/servers/entrypoint.sh b/servers/entrypoint.sh index 12109a6..d5ca02b 100644 --- a/servers/entrypoint.sh +++ b/servers/entrypoint.sh @@ -1,11 +1,8 @@ #!/bin/sh set -e -# Default to deployed if not provided by runtime -MCP_ENTRY_VAL="${MCP_ENTRY:-deployed}" - -# Build ASGI target module path -APP_MODULE="${MCP_ENTRY_VAL}_mcp:app" +: "${MCP_ENTRY:=deployed_mcp}" +APP_MODULE="${MCP_ENTRY}:app" echo "Starting uvicorn with module: ${APP_MODULE}" exec uvicorn "${APP_MODULE}" --host 0.0.0.0 --port 8000 diff --git a/spanish/README.md b/spanish/README.md index 8c8cf13..fb1e247 100644 --- a/spanish/README.md +++ b/spanish/README.md @@ -520,7 +520,7 @@ Después del despliegue, podés probar localmente con OAuth habilitado: ```bash # Correr el servidor MCP -cd servers && uvicorn auth_mcp:app --host 0.0.0.0 --port 8000 +cd servers && uvicorn auth_entra_mcp:app --host 0.0.0.0 --port 8000 ``` El servidor usará la App Registration de Entra para OAuth y CosmosDB para almacenamiento de clientes. diff --git a/uv.lock b/uv.lock index 2dd831e..abdb12d 100644 --- a/uv.lock +++ b/uv.lock @@ -361,7 +361,7 @@ wheels = [ [[package]] name = "azure-monitor-opentelemetry" -version = "1.8.1" +version = "1.8.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -377,27 +377,26 @@ dependencies = [ { name = "opentelemetry-resource-detector-azure" }, { name = "opentelemetry-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/ae/eae89705498c975b1cfcc2ce0e5bfbe784a47ffd54cef6fbebe31fdb2295/azure_monitor_opentelemetry-1.8.1.tar.gz", hash = "sha256:9b93b62868775d74db60d9e997cfccc5898260c5de23278d7e99cce3764e9fda", size = 53471 } +sdist = { url = "https://files.pythonhosted.org/packages/8d/a9/f335c32e76e3bac3fbbd7977980f62a7deec5191e984517bdbb38539dfd1/azure_monitor_opentelemetry-1.8.3.tar.gz", hash = "sha256:4aa10f6712db653f618e14e3701de7a2f96669a8f2fea6fb22125077da4ea91c", size = 55177 } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/ab/d063f5d0debbb01ef716789f5b4b315d58f657dd5dbf15e47ca6648a557b/azure_monitor_opentelemetry-1.8.1-py3-none-any.whl", hash = "sha256:bebca6af9d81ddc52df59b281a5acc84182bbf1cbccd6f843a2074f6e283947e", size = 27169 }, + { url = "https://files.pythonhosted.org/packages/57/4f/138e2f1eddce9b8dda1cccccb5eb3819c1d4d5ea843fbf09ecc3d810641b/azure_monitor_opentelemetry-1.8.3-py3-none-any.whl", hash = "sha256:647248328bb03f8044918411d57c661230277958559f067892bd79f98ce8f69c", size = 27687 }, ] [[package]] name = "azure-monitor-opentelemetry-exporter" -version = "1.0.0b44" +version = "1.0.0b46" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, { name = "azure-identity" }, - { name = "fixedint" }, { name = "msrest" }, { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, { name = "psutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3e/9a/acb253869ef59482c628f4dc7e049323d0026a9374adf7b398d0b04b6094/azure_monitor_opentelemetry_exporter-1.0.0b44.tar.gz", hash = "sha256:9b0f430a6a46a78bf757ae301488c10c1996f1bd6c5c01a07b9d33583cc4fa4b", size = 271712 } +sdist = { url = "https://files.pythonhosted.org/packages/63/57/4dd223fcded4955f85ecfae721802cf4bc5a9a95efd8a9a00271f80d4e6b/azure_monitor_opentelemetry_exporter-1.0.0b46.tar.gz", hash = "sha256:a2fd5837c41b5b10316b089ccbe694fc8a69c23db92a5555b298b1eec3eb38bd", size = 277957 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/46/31809698a0d50559fde108a4f4cb2d9532967ae514a113dba39763e048b7/azure_monitor_opentelemetry_exporter-1.0.0b44-py2.py3-none-any.whl", hash = "sha256:82d23081bf007acab8d4861229ab482e4666307a29492fbf0bf19981b4d37024", size = 198516 }, + { url = "https://files.pythonhosted.org/packages/9b/f9/5b1273d134743b59a271000c08ea3b686f184bfbd73e5ab3a7feae2d0e5f/azure_monitor_opentelemetry_exporter-1.0.0b46-py2.py3-none-any.whl", hash = "sha256:12935e72dcad4a162636eaa5f861e106fcdc3c19928e79cd58b52653fe15625a", size = 200542 }, ] [[package]] @@ -520,6 +519,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295 }, ] +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -695,6 +703,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317 }, ] +[[package]] +name = "fakeredis" +version = "2.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/f9/57464119936414d60697fcbd32f38909bb5688b616ae13de6e98384433e0/fakeredis-2.33.0.tar.gz", hash = "sha256:d7bc9a69d21df108a6451bbffee23b3eba432c21a654afc7ff2d295428ec5770", size = 175187 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/78/a850fed8aeef96d4a99043c90b818b2ed5419cd5b24a4049fd7cfb9f1471/fakeredis-2.33.0-py3-none-any.whl", hash = "sha256:de535f3f9ccde1c56672ab2fdd6a8efbc4f2619fc2f1acc87b8737177d71c965", size = 119605 }, +] + +[package.optional-dependencies] +lua = [ + { name = "lupa" }, +] + [[package]] name = "fastapi" version = "0.119.1" @@ -711,7 +737,7 @@ wheels = [ [[package]] name = "fastmcp" -version = "2.13.3" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, @@ -722,17 +748,18 @@ dependencies = [ { name = "mcp" }, { name = "openapi-pydantic" }, { name = "platformdirs" }, - { name = "py-key-value-aio", extra = ["disk", "memory"] }, + { name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] }, { name = "pydantic", extra = ["email"] }, + { name = "pydocket" }, { name = "pyperclip" }, { name = "python-dotenv" }, { name = "rich" }, { name = "uvicorn" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/a1/a507bfb73f51983759cbbc3702b6f4780128cff68ebbc51db2f10170c950/fastmcp-2.13.3.tar.gz", hash = "sha256:ebca59e99412c596dd75ebdd5147800f6abc2490d025af76fa8ea4fc5f68781d", size = 8185958 } +sdist = { url = "https://files.pythonhosted.org/packages/d1/1e/e3528227688c248283f6d86869b1e900563ffc223eff00f4f923d2750365/fastmcp-2.14.2.tar.gz", hash = "sha256:bd23d1b808b6f446444f10114dac468b11bfb9153ed78628f5619763d0cf573e", size = 8272966 } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/bc/56925f1202357dbfcfdfd0c75afc6c27ec1e6ef1d89b7e7410df3945ceb4/fastmcp-2.13.3-py3-none-any.whl", hash = "sha256:5173d335f4e6aabcfb5a5131af3fa092f604b303130fd3a49226b7a844a48e65", size = 385644 }, + { url = "https://files.pythonhosted.org/packages/0d/67/8456d39484fcb7afd0defed21918e773ed59a98b39e5b633328527c88367/fastmcp-2.14.2-py3-none-any.whl", hash = "sha256:e33cd622e1ebd5110af6a981804525b6cd41072e3c7d68268ed69ef3be651aca", size = 413279 }, ] [[package]] @@ -744,15 +771,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054 }, ] -[[package]] -name = "fixedint" -version = "0.1.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/32/c6/b1b9b3f69915d51909ef6ebe6352e286ec3d6f2077278af83ec6e3cc569c/fixedint-0.1.6.tar.gz", hash = "sha256:703005d090499d41ce7ce2ee7eae8f7a5589a81acdc6b79f1728a56495f2c799", size = 12750 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/6d/8f5307d26ce700a89e5a67d1e1ad15eff977211f9ed3ae90d7b0d67f4e66/fixedint-0.1.6-py3-none-any.whl", hash = "sha256:b8cf9f913735d2904deadda7a6daa9f57100599da1de57a7448ea1be75ae8c9c", size = 12702 }, -] - [[package]] name = "frozenlist" version = "1.8.0" @@ -1012,6 +1030,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/7d/41acf8e22d791bde812cb6c2c36128bb932ed8ae066bcb5e39cb198e8253/jaraco_context-6.0.2.tar.gz", hash = "sha256:953ae8dddb57b1d791bf72ea1009b32088840a7dd19b9ba16443f62be919ee57", size = 14994 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0c/1e0096ced9c55f9c6c6655446798df74165780375d3f5ab5f33751e087ae/jaraco_context-6.0.2-py3-none-any.whl", hash = "sha256:55fc21af4b4f9ca94aa643b6ee7fe13b1e4c01abf3aeb98ca4ad9c80b741c786", size = 6988 }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481 }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010 }, +] + [[package]] name = "jiter" version = "0.11.1" @@ -1121,6 +1181,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 }, ] +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160 }, +] + [[package]] name = "langchain" version = "1.0.0a5" @@ -1270,7 +1347,7 @@ wheels = [ [[package]] name = "logfire" -version = "4.15.1" +version = "4.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "executing" }, @@ -1281,9 +1358,28 @@ dependencies = [ { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/7b/3cdbfbd8fc085912d7322afc5f16ffd396197574fc9c786422b0ce3f5232/logfire-4.15.1.tar.gz", hash = "sha256:fac8463c0319af6d1bf66788802ff0a04b481dac006564f6837f64c7404f474a", size = 549319 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/74/88e8fec6e3fe247a7cdaeba777cbf7aced11de33db423693690e20ab8356/logfire-4.17.0.tar.gz", hash = "sha256:693d47d6b8b0a8f9fd8112d958eb9e3ae00fe5d323e1eee468e4bc6379bb2c4f", size = 558897 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/68/c49baec77c56dcf5698daaa66a097523063d55b2feaecc968000518521a1/logfire-4.17.0-py3-none-any.whl", hash = "sha256:1ed1064a4126f48503f7832ce6039425dff70128d145552d7981ac9ce2151b17", size = 232456 }, +] + +[[package]] +name = "lupa" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282 } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/44/45c39998cb3920a11498455f3f62f173bd3f5d9a15bd0db40f88bedb9e1f/logfire-4.15.1-py3-none-any.whl", hash = "sha256:b931d2becf937c08d7c89f1ab68ab05298095b010dfaf29cbd22f7bcacbaa2bb", size = 228716 }, + { url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232 }, + { url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625 }, + { url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057 }, + { url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227 }, + { url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752 }, + { url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009 }, + { url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301 }, + { url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673 }, + { url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227 }, + { url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558 }, + { url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424 }, ] [[package]] @@ -1300,7 +1396,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.22.0" +version = "1.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1318,9 +1414,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/a2/c5ec0ab38b35ade2ae49a90fada718fbc76811dc5aa1760414c6aaa6b08a/mcp-1.22.0.tar.gz", hash = "sha256:769b9ac90ed42134375b19e777a2858ca300f95f2e800982b3e2be62dfc0ba01", size = 471788 } +sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/bb/711099f9c6bb52770f56e56401cdfb10da5b67029f701e0df29362df4c8e/mcp-1.22.0-py3-none-any.whl", hash = "sha256:bed758e24df1ed6846989c909ba4e3df339a27b4f30f1b8b627862a4bade4e98", size = 175489 }, + { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076 }, ] [package.optional-dependencies] @@ -1507,6 +1603,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/38/6266604dffb43378055394ea110570cf261a49876fc48f548dfe876f34cc/ml_dtypes-0.5.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdf40d2aaabd3913dec11840f0d0ebb1b93134f99af6a0a4fd88ffe924928ab4", size = 5285422 }, ] +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667 }, +] + [[package]] name = "msal" version = "1.34.0" @@ -1707,32 +1812,32 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.38.0" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/0b/e5428c009d4d9af0515b0a8371a8aaae695371af291f45e702f7969dce6b/opentelemetry_api-1.39.0.tar.gz", hash = "sha256:6130644268c5ac6bdffaf660ce878f10906b3e789f7e2daa5e169b047a2933b9", size = 65763 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947 }, + { url = "https://files.pythonhosted.org/packages/05/85/d831a9bc0a9e0e1a304ff3d12c1489a5fbc9bf6690a15dcbdae372bbca45/opentelemetry_api-1.39.0-py3-none-any.whl", hash = "sha256:3c3b3ca5c5687b1b5b37e5c5027ff68eacea8675241b29f13110a8ffbb8f0459", size = 66357 }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.38.0" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431 } +sdist = { url = "https://files.pythonhosted.org/packages/11/cb/3a29ce606b10c76d413d6edd42d25a654af03e73e50696611e757d2602f3/opentelemetry_exporter_otlp_proto_common-1.39.0.tar.gz", hash = "sha256:a135fceed1a6d767f75be65bd2845da344dd8b9258eeed6bc48509d02b184409", size = 20407 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359 }, + { url = "https://files.pythonhosted.org/packages/ef/c6/215edba62d13a3948c718b289539f70e40965bc37fc82ecd55bb0b749c1a/opentelemetry_exporter_otlp_proto_common-1.39.0-py3-none-any.whl", hash = "sha256:3d77be7c4bdf90f1a76666c934368b8abed730b5c6f0547a2ec57feb115849ac", size = 18367 }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.38.0" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -1743,14 +1848,14 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/c0/43222f5b97dc10812bc4f0abc5dc7cd0a2525a91b5151d26c9e2e958f52e/opentelemetry_exporter_otlp_proto_grpc-1.38.0.tar.gz", hash = "sha256:2473935e9eac71f401de6101d37d6f3f0f1831db92b953c7dcc912536158ebd6", size = 24676 } +sdist = { url = "https://files.pythonhosted.org/packages/7e/62/4db083ee9620da3065eeb559e9fc128f41a1d15e7c48d7c83aafbccd354c/opentelemetry_exporter_otlp_proto_grpc-1.39.0.tar.gz", hash = "sha256:7e7bb3f436006836c0e0a42ac619097746ad5553ad7128a5bd4d3e727f37fc06", size = 24650 } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/f0/bd831afbdba74ca2ce3982142a2fad707f8c487e8a3b6fef01f1d5945d1b/opentelemetry_exporter_otlp_proto_grpc-1.38.0-py3-none-any.whl", hash = "sha256:7c49fd9b4bd0dbe9ba13d91f764c2d20b0025649a6e4ac35792fb8d84d764bc7", size = 19695 }, + { url = "https://files.pythonhosted.org/packages/56/e8/d420b94ffddfd8cff85bb4aa5d98da26ce7935dc3cf3eca6b83cd39ab436/opentelemetry_exporter_otlp_proto_grpc-1.39.0-py3-none-any.whl", hash = "sha256:758641278050de9bb895738f35ff8840e4a47685b7e6ef4a201fe83196ba7a05", size = 19765 }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.38.0" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -1761,14 +1866,28 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/0a/debcdfb029fbd1ccd1563f7c287b89a6f7bef3b2902ade56797bfd020854/opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b", size = 17282 } +sdist = { url = "https://files.pythonhosted.org/packages/81/dc/1e9bf3f6a28e29eba516bc0266e052996d02bc7e92675f3cd38169607609/opentelemetry_exporter_otlp_proto_http-1.39.0.tar.gz", hash = "sha256:28d78fc0eb82d5a71ae552263d5012fa3ebad18dfd189bf8d8095ba0e65ee1ed", size = 17287 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/46/e4a102e17205bb05a50dbf24ef0e92b66b648cd67db9a68865af06a242fd/opentelemetry_exporter_otlp_proto_http-1.39.0-py3-none-any.whl", hash = "sha256:5789cb1375a8b82653328c0ce13a054d285f774099faf9d068032a49de4c7862", size = 19639 }, +] + +[[package]] +name = "opentelemetry-exporter-prometheus" +version = "0.60b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "prometheus-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/da/8b81ff9d045fae7cac1e8bbf7ad59bf113d1008e483fc03e2cd3a0e620a3/opentelemetry_exporter_prometheus-0.60b0.tar.gz", hash = "sha256:c6ae33e52cdd1dbfed1f7436935df94eb03c725b57322026d04e6fbc37108e6e", size = 14975 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/77/154004c99fb9f291f74aa0822a2f5bbf565a72d8126b3a1b63ed8e5f83c7/opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b", size = 19579 }, + { url = "https://files.pythonhosted.org/packages/8b/18/18b662a6ecb8252db9e7457fd3c836729bf28b055b60505cbd4763ea9300/opentelemetry_exporter_prometheus-0.60b0-py3-none-any.whl", hash = "sha256:4f616397040257fae4c5e5272b57b47c13372e3b7f0f2db2427fd4dbe69c60b5", size = 13017 }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1776,14 +1895,14 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/ed/9c65cd209407fd807fa05be03ee30f159bdac8d59e7ea16a8fe5a1601222/opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc", size = 31544 } +sdist = { url = "https://files.pythonhosted.org/packages/55/3c/bd53dbb42eff93d18e3047c7be11224aa9966ce98ac4cc5bfb860a32c95a/opentelemetry_instrumentation-0.60b0.tar.gz", hash = "sha256:4e9fec930f283a2677a2217754b40aaf9ef76edae40499c165bc7f1d15366a74", size = 31707 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/f5/7a40ff3f62bfe715dad2f633d7f1174ba1a7dd74254c15b2558b3401262a/opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee", size = 33020 }, + { url = "https://files.pythonhosted.org/packages/5c/7b/5b5b9f8cfe727a28553acf9cd287b1d7f706f5c0a00d6e482df55b169483/opentelemetry_instrumentation-0.60b0-py3-none-any.whl", hash = "sha256:aaafa1483543a402819f1bdfb06af721c87d60dd109501f9997332862a35c76a", size = 33096 }, ] [[package]] name = "opentelemetry-instrumentation-asgi" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, @@ -1792,14 +1911,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/a4/cfbb6fc1ec0aa9bf5a93f548e6a11ab3ac1956272f17e0d399aa2c1f85bc/opentelemetry_instrumentation_asgi-0.59b0.tar.gz", hash = "sha256:2509d6fe9fd829399ce3536e3a00426c7e3aa359fc1ed9ceee1628b56da40e7a", size = 25116 } +sdist = { url = "https://files.pythonhosted.org/packages/b0/0a/715ea7044708d3c215385fb2a1c6ffe429aacb3cd23a348060aaeda52834/opentelemetry_instrumentation_asgi-0.60b0.tar.gz", hash = "sha256:928731218050089dca69f0fe980b8bfe109f384be8b89802d7337372ddb67b91", size = 26083 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/88/fe02d809963b182aafbf5588685d7a05af8861379b0ec203d48e360d4502/opentelemetry_instrumentation_asgi-0.59b0-py3-none-any.whl", hash = "sha256:ba9703e09d2c33c52fa798171f344c8123488fcd45017887981df088452d3c53", size = 16797 }, + { url = "https://files.pythonhosted.org/packages/9b/8c/c6c59127fd996107243ca45669355665a7daff578ddafb86d6d2d3b01428/opentelemetry_instrumentation_asgi-0.60b0-py3-none-any.whl", hash = "sha256:9d76a541269452c718a0384478f3291feb650c5a3f29e578fdc6613ea3729cf3", size = 16907 }, ] [[package]] name = "opentelemetry-instrumentation-dbapi" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1807,14 +1926,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/aa/36a09652c98c65b42408d40f222fba031a3a281f1b6682e1b141b20b508d/opentelemetry_instrumentation_dbapi-0.59b0.tar.gz", hash = "sha256:c50112ae1cdb7f55bddcf57eca96aaa0f2dd78732be2b00953183439a4740493", size = 16308 } +sdist = { url = "https://files.pythonhosted.org/packages/12/7f/b4c1fbce01b29daad5ef1396427c9cd3c7a55ee68e75f8c11089c7e2533d/opentelemetry_instrumentation_dbapi-0.60b0.tar.gz", hash = "sha256:2b7eb38e46890cebe5bc1a1c03d2ab07fc159b0b7b91342941ee33dd73876d84", size = 16311 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/9b/1739b5b7926cbae342880d7a56d59a847313e6568a96ba7d4873ce0c0996/opentelemetry_instrumentation_dbapi-0.59b0-py3-none-any.whl", hash = "sha256:672d59caa06754b42d4e722644d9fcd00a1f9f862e9ea5cef6d4da454515ac67", size = 13970 }, + { url = "https://files.pythonhosted.org/packages/23/0a/65e100c6d803de59a9113a993dcd371a4027453ba15ce4dabdb0343ca154/opentelemetry_instrumentation_dbapi-0.60b0-py3-none-any.whl", hash = "sha256:429d8ca34a44a4296b9b09a1bd373fff350998d200525c6e79883c3328559b03", size = 13966 }, ] [[package]] name = "opentelemetry-instrumentation-django" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1823,14 +1942,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/cf/a329abb33a9f7934cfd9e5645e69550a4d5dcdd6d1970283854460e11f9d/opentelemetry_instrumentation_django-0.59b0.tar.gz", hash = "sha256:469c2d973619355645ec696bbc4afab836ce22cbc83236a0382c3090588f7772", size = 25008 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/d2/8ddd9a5c61cd5048d422be8d22fac40f603aa82f0babf9f7c40db871080c/opentelemetry_instrumentation_django-0.60b0.tar.gz", hash = "sha256:461e6fca27936ba97eec26da38bb5f19310783370478c7ca3a3e40faaceac9cc", size = 26596 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/c0/c8980bcb1ef1263fe0f4bbe52b74a1442c29b35eca4a9cb4ab4bb1028a3c/opentelemetry_instrumentation_django-0.59b0-py3-none-any.whl", hash = "sha256:a0a9eb74afc3870e72eaaa776054fbfd4d83ae306d0c5995f14414bcef2d830e", size = 19595 }, + { url = "https://files.pythonhosted.org/packages/18/d6/28684547bf6c699582e998a172ba8bb08405cf6706729b0d6a16042e998f/opentelemetry_instrumentation_django-0.60b0-py3-none-any.whl", hash = "sha256:95495649c8c34ce9217c6873cdd10fc4fcaa67c25f8329adc54f5b286999e40b", size = 21169 }, ] [[package]] name = "opentelemetry-instrumentation-fastapi" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1839,14 +1958,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/a7/7a6ce5009584ce97dbfd5ce77d4f9d9570147507363349d2cb705c402bcf/opentelemetry_instrumentation_fastapi-0.59b0.tar.gz", hash = "sha256:e8fe620cfcca96a7d634003df1bc36a42369dedcdd6893e13fb5903aeeb89b2b", size = 24967 } +sdist = { url = "https://files.pythonhosted.org/packages/fe/51/a021a7c929b5103fcb6bfdfa5a99abcaeb3b505faf9e3ee3ec14612c1ef9/opentelemetry_instrumentation_fastapi-0.60b0.tar.gz", hash = "sha256:5d34d67eb634a08bfe9e530680d6177521cd9da79285144e6d5a8f42683ed1b3", size = 24960 } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/27/5914c8bf140ffc70eff153077e225997c7b054f0bf28e11b9ab91b63b18f/opentelemetry_instrumentation_fastapi-0.59b0-py3-none-any.whl", hash = "sha256:0d8d00ff7d25cca40a4b2356d1d40a8f001e0668f60c102f5aa6bb721d660c4f", size = 13492 }, + { url = "https://files.pythonhosted.org/packages/b1/5a/e238c108eb65a726d75184439377a87d532050036b54e718e4c789b26d1a/opentelemetry_instrumentation_fastapi-0.60b0-py3-none-any.whl", hash = "sha256:415c6602db01ee339276ea4cabe3e80177c9e955631c087f2ef60a75e31bfaee", size = 13478 }, ] [[package]] name = "opentelemetry-instrumentation-flask" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1856,28 +1975,28 @@ dependencies = [ { name = "opentelemetry-util-http" }, { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/42/afccc8414f85108d41bb73155d0e828bf07102068ef03396bd1ef4296544/opentelemetry_instrumentation_flask-0.59b0.tar.gz", hash = "sha256:8b379d331b61f40a7c72c9ae8e0fca72c72ffeb6db75908811217196c9544b9b", size = 19587 } +sdist = { url = "https://files.pythonhosted.org/packages/30/cc/e0758c23d66fd49956169cb24b5b06130373da2ce8d49945abce82003518/opentelemetry_instrumentation_flask-0.60b0.tar.gz", hash = "sha256:560f08598ef40cdcf7ca05bfb2e3ea74fab076e676f4c18bb36bb379bf5c4a1b", size = 20336 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/5e/99db8cedd745d989f860a8c9544c6d5c47c79117251088927e98c7167f85/opentelemetry_instrumentation_flask-0.59b0-py3-none-any.whl", hash = "sha256:5e97fde228f66d7bf9512a86383c0d30a869e2d3b424b51a2781ca40d0287cdc", size = 14741 }, + { url = "https://files.pythonhosted.org/packages/9b/b5/387ce11f59e5ce65b890adc3f9c457877143b8a6d107a3a0b305397933a1/opentelemetry_instrumentation_flask-0.60b0-py3-none-any.whl", hash = "sha256:106e5774f79ac9b86dd0d949c1b8f46c807a8af16184301e10d24fc94e680d04", size = 15189 }, ] [[package]] name = "opentelemetry-instrumentation-psycopg2" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-dbapi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/76/d4adf1b9e811ee6af19b074d80cff1026f3074f78d2d915846aecbab29d9/opentelemetry_instrumentation_psycopg2-0.59b0.tar.gz", hash = "sha256:ba440b15543a7e8c6ffd1f20a30e6062cbf34cc42e61c602b8587b512704588b", size = 10735 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/68/5ae8a3b9a28c2fdf8d3d050e451ddb2612ca963679b08a2959f01f6dda4b/opentelemetry_instrumentation_psycopg2-0.60b0.tar.gz", hash = "sha256:59e527fd97739440380634ffcf9431aa7f2965d939d8d5829790886e2b54ede9", size = 11266 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/70/3ac33f00c928725fb52bb9eaf2b51ac57370dfd9eb8ddb60d6fd6e9fab95/opentelemetry_instrumentation_psycopg2-0.59b0-py3-none-any.whl", hash = "sha256:c96e1f5d91320166173af4ca8f4735ec2de61b7d99810bd23dd44644334514bd", size = 10731 }, + { url = "https://files.pythonhosted.org/packages/d4/24/66b5a41a2b0d1d07cc9b0fbd80f8b5c66b46a4d4731743505891da8b3cbe/opentelemetry_instrumentation_psycopg2-0.60b0-py3-none-any.whl", hash = "sha256:ea136a32babd559aa717c04dddf6aa78aa94b816fb4e10dfe06751727ef306d4", size = 11284 }, ] [[package]] name = "opentelemetry-instrumentation-requests" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1885,14 +2004,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/01/31282a46b09684dfc636bc066deb090bae6973e71e85e253a8c74e727b1f/opentelemetry_instrumentation_requests-0.59b0.tar.gz", hash = "sha256:9af2ffe3317f03074d7f865919139e89170b6763a0251b68c25e8e64e04b3400", size = 15186 } +sdist = { url = "https://files.pythonhosted.org/packages/26/0f/94c6181e95c867f559715887c418170a9eadd92ea6090122d464e375ff56/opentelemetry_instrumentation_requests-0.60b0.tar.gz", hash = "sha256:5079ed8df96d01dab915a0766cd28a49be7c33439ce43d6d39843ed6dee3204f", size = 16173 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/ea/c282ba418b2669e4f730cb3f68b02a0ca65f4baf801e971169a4cc449ffb/opentelemetry_instrumentation_requests-0.59b0-py3-none-any.whl", hash = "sha256:d43121532877e31a46c48649279cec2504ee1e0ceb3c87b80fe5ccd7eafc14c1", size = 12966 }, + { url = "https://files.pythonhosted.org/packages/f1/e1/2f13b41c5679243ba8eae651170c4ce2f532349877819566ae4a89a2b47f/opentelemetry_instrumentation_requests-0.60b0-py3-none-any.whl", hash = "sha256:e9957f3a650ae55502fa227b29ff985b37d63e41c85e6e1555d48039f092ea83", size = 13122 }, ] [[package]] name = "opentelemetry-instrumentation-starlette" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1901,14 +2020,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/39/95e5868e731bcf08a642483afaa34c455bfa4f61d6cfdd096626e9ee40d6/opentelemetry_instrumentation_starlette-0.59b0.tar.gz", hash = "sha256:3f033fd92d6a8e4122ebcb3d83afc5c64d6be7930e9094876eb02b8afbd08ba5", size = 14657 } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8f/8113a6cfcf928c1175c7c5ae0266ea2a9aa9814aca304d5555202a137a6c/opentelemetry_instrumentation_starlette-0.60b0.tar.gz", hash = "sha256:f446c1a928cbc46178a90a351c6d6d78629777760ac132c2b4342d1c5aebe1f3", size = 14643 } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/fc/4727e11d7c442ba8bb79ba261303c9e061c235eabcf633ddefc10230c8c4/opentelemetry_instrumentation_starlette-0.59b0-py3-none-any.whl", hash = "sha256:a833b97d297e4b2aaf58041612663dbabb8380e3993c76a4e32b3e351470f321", size = 11778 }, + { url = "https://files.pythonhosted.org/packages/de/8e/3b3cb695b9d6e53f996cedb6d2873391baf452c784573791f9fcc02edf41/opentelemetry_instrumentation_starlette-0.60b0-py3-none-any.whl", hash = "sha256:efdbcf71a44e4c4d2aa506f891c98e966e09c507fa4a5a33f2e45b5424b55f36", size = 11764 }, ] [[package]] name = "opentelemetry-instrumentation-urllib" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1916,14 +2035,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/85/70cc79162aa778179520b82234e3a8668f0aea67a279bd81a2522868687d/opentelemetry_instrumentation_urllib-0.59b0.tar.gz", hash = "sha256:1e2bb3427ce13854453777d8dccf3b0144640b03846f00fc302bdb6e1f2f8c7a", size = 13931 } +sdist = { url = "https://files.pythonhosted.org/packages/95/db/be895de04bd56d7a2b2ef6d267a4c52f6cd325b6647d1c15ae888b1b0f6a/opentelemetry_instrumentation_urllib-0.60b0.tar.gz", hash = "sha256:89b8796f9ab64d0ea0833cfea98745963baa0d7e4a775b3d2a77791aa97cf3f9", size = 13931 } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/94/0e87ffe1edfdda27e401d8ebab71ee3dd9ceaac11f98b8f5c190820a317f/opentelemetry_instrumentation_urllib-0.59b0-py3-none-any.whl", hash = "sha256:ed2bd1a02e4334c13c13033681ff8cf10d5dfcd5b0e6d7514f94a00e7f7bd671", size = 12672 }, + { url = "https://files.pythonhosted.org/packages/2b/e0/178914d5cec77baef797c6d47412da478ff871b05eb8732d64037b87c868/opentelemetry_instrumentation_urllib-0.60b0-py3-none-any.whl", hash = "sha256:80e3545d02505dc0ea61b3a0a141ec2828e11bee6b7dedfd3ee7ed9a7adbf862", size = 12673 }, ] [[package]] name = "opentelemetry-instrumentation-urllib3" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1932,14 +2051,14 @@ dependencies = [ { name = "opentelemetry-util-http" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/53/ff93665911808933b1af6fbbb1be2eb83c0c46e3b5f24b0b04c094b5b719/opentelemetry_instrumentation_urllib3-0.59b0.tar.gz", hash = "sha256:2de8d53a746bba043be1bc8f3246e1b131ebb6e94fe73601edd8b2bd91fe35b8", size = 15788 } +sdist = { url = "https://files.pythonhosted.org/packages/25/a8/16a32239e84741fae1a2932badeade5e72b73bfc331b53f7049a648ca00b/opentelemetry_instrumentation_urllib3-0.60b0.tar.gz", hash = "sha256:6ae1640a993901bae8eda5496d8b1440fb326a29e4ba1db342738b8868174aad", size = 15789 } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/3d/673cbea7aafb93a4613abf3d9c920d7c65a8cad79c910719dc286169bac8/opentelemetry_instrumentation_urllib3-0.59b0-py3-none-any.whl", hash = "sha256:a68c363092cf5db8c67c5778dbb2e4a14554e77baf7d276c374ea75ec926e148", size = 13187 }, + { url = "https://files.pythonhosted.org/packages/16/b2/ca27479eaf1f3f4825481769eb0cb200cad839040b8d5f42662d0398a256/opentelemetry_instrumentation_urllib3-0.60b0-py3-none-any.whl", hash = "sha256:9a07504560feae650a9205b3e2a579a835819bb1d55498d26a5db477fe04bba0", size = 13187 }, ] [[package]] name = "opentelemetry-instrumentation-wsgi" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -1947,21 +2066,21 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/1d/595907631263e0e4a9e3d5b2958b9ecfe3872938c706e6c842d0767c798c/opentelemetry_instrumentation_wsgi-0.59b0.tar.gz", hash = "sha256:ff0c3df043bd3653ad6a543cb2a1e666fbd4d63efffa04fa9d9090cef462e798", size = 18377 } +sdist = { url = "https://files.pythonhosted.org/packages/10/ad/ae04e35f3b96d9c20d5d3df94a4c296eabf7a54d35d6c831179471128270/opentelemetry_instrumentation_wsgi-0.60b0.tar.gz", hash = "sha256:5815195b1b9890f55c4baafec94ff98591579a7d9b16256064adea8ee5784651", size = 19104 } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/06/ef769a4f6fde97ff58bc4e38a12b6ae4be1d5fe0f76e69c19b0fd2e10405/opentelemetry_instrumentation_wsgi-0.59b0-py3-none-any.whl", hash = "sha256:f271076e56c22da1d0d3404519ba4a1891b39ee3d470ca7ece7332d57cbaa6b9", size = 14447 }, + { url = "https://files.pythonhosted.org/packages/73/0e/1ed4d3cdce7b2e00a24f79933b3472e642d4db98aaccc09769be5cbe5296/opentelemetry_instrumentation_wsgi-0.60b0-py3-none-any.whl", hash = "sha256:0ff80614c1e73f7e94a5860c7e6222a51195eebab3dc5f50d89013db3d5d2f13", size = 14553 }, ] [[package]] name = "opentelemetry-proto" -version = "1.38.0" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152 } +sdist = { url = "https://files.pythonhosted.org/packages/48/b5/64d2f8c3393cd13ea2092106118f7b98461ba09333d40179a31444c6f176/opentelemetry_proto-1.39.0.tar.gz", hash = "sha256:c1fa48678ad1a1624258698e59be73f990b7fc1f39e73e16a9d08eef65dd838c", size = 46153 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535 }, + { url = "https://files.pythonhosted.org/packages/e3/4d/d500e1862beed68318705732d1976c390f4a72ca8009c4983ff627acff20/opentelemetry_proto-1.39.0-py3-none-any.whl", hash = "sha256:1e086552ac79acb501485ff0ce75533f70f3382d43d0a30728eeee594f7bf818", size = 72534 }, ] [[package]] @@ -1978,29 +2097,29 @@ wheels = [ [[package]] name = "opentelemetry-sdk" -version = "1.38.0" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942 } +sdist = { url = "https://files.pythonhosted.org/packages/51/e3/7cd989003e7cde72e0becfe830abff0df55c69d237ee7961a541e0167833/opentelemetry_sdk-1.39.0.tar.gz", hash = "sha256:c22204f12a0529e07aa4d985f1bca9d6b0e7b29fe7f03e923548ae52e0e15dde", size = 171322 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349 }, + { url = "https://files.pythonhosted.org/packages/a4/b4/2adc8bc83eb1055ecb592708efb6f0c520cc2eb68970b02b0f6ecda149cf/opentelemetry_sdk-1.39.0-py3-none-any.whl", hash = "sha256:90cfb07600dfc0d2de26120cebc0c8f27e69bf77cd80ef96645232372709a514", size = 132413 }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861 } +sdist = { url = "https://files.pythonhosted.org/packages/71/0e/176a7844fe4e3cb5de604212094dffaed4e18b32f1c56b5258bcbcba85c2/opentelemetry_semantic_conventions-0.60b0.tar.gz", hash = "sha256:227d7aa73cbb8a2e418029d6b6465553aa01cf7e78ec9d0bc3255c7b3ac5bf8f", size = 137935 } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954 }, + { url = "https://files.pythonhosted.org/packages/d0/56/af0306666f91bae47db14d620775604688361f0f76a872e0005277311131/opentelemetry_semantic_conventions-0.60b0-py3-none-any.whl", hash = "sha256:069530852691136018087b52688857d97bba61cd641d0f8628d2d92788c4f78a", size = 219981 }, ] [[package]] @@ -2014,11 +2133,11 @@ wheels = [ [[package]] name = "opentelemetry-util-http" -version = "0.59b0" +version = "0.60b0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/f7/13cd081e7851c42520ab0e96efb17ffbd901111a50b8252ec1e240664020/opentelemetry_util_http-0.59b0.tar.gz", hash = "sha256:ae66ee91be31938d832f3b4bc4eb8a911f6eddd38969c4a871b1230db2a0a560", size = 9412 } +sdist = { url = "https://files.pythonhosted.org/packages/38/0d/786a713445cf338131fef3a84fab1378e4b2ef3c3ea348eeb0c915eb804a/opentelemetry_util_http-0.60b0.tar.gz", hash = "sha256:e42b7bb49bba43b6f34390327d97e5016eb1c47949ceaf37c4795472a4e3a82d", size = 10576 } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/56/62282d1d4482061360449dacc990c89cad0fc810a2ed937b636300f55023/opentelemetry_util_http-0.59b0-py3-none-any.whl", hash = "sha256:6d036a07563bce87bf521839c0671b507a02a0d39d7ea61b88efa14c6e25355d", size = 7648 }, + { url = "https://files.pythonhosted.org/packages/53/5d/a448862f6d10c95685ed0e703596b6bd1784074e7ad90bffdc550abb7b68/opentelemetry_util_http-0.60b0-py3-none-any.whl", hash = "sha256:4f366f1a48adb74ffa6f80aee26f96882e767e01b03cd1cfb948b6e1020341fe", size = 8742 }, ] [[package]] @@ -2151,6 +2270,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429 }, ] +[[package]] +name = "prometheus-client" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145 }, +] + [[package]] name = "propcache" version = "0.4.1" @@ -2250,9 +2378,15 @@ disk = [ { name = "diskcache" }, { name = "pathvalidate" }, ] +keyring = [ + { name = "keyring" }, +] memory = [ { name = "cachetools" }, ] +redis = [ + { name = "redis" }, +] [[package]] name = "py-key-value-shared" @@ -2369,6 +2503,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608 }, ] +[[package]] +name = "pydocket" +version = "0.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "fakeredis", extra = ["lua"] }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-prometheus" }, + { name = "opentelemetry-instrumentation" }, + { name = "prometheus-client" }, + { name = "py-key-value-aio", extra = ["memory", "redis"] }, + { name = "python-json-logger" }, + { name = "redis" }, + { name = "rich" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/c5/61dcfce4d50b66a3f09743294d37fab598b81bb0975054b7f732da9243ec/pydocket-0.16.3.tar.gz", hash = "sha256:78e9da576de09e9f3f410d2471ef1c679b7741ddd21b586c97a13872b69bd265", size = 297080 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/94/93b7f5981aa04f922e0d9ce7326a4587866ec7e39f7c180ffcf408e66ee8/pydocket-0.16.3-py3-none-any.whl", hash = "sha256:e2b50925356e7cd535286255195458ac7bba15f25293356651b36d223db5dd7c", size = 67087 }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -2422,6 +2579,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 }, ] +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548 }, +] + [[package]] name = "python-mcp-demos" version = "0.1.0" @@ -2460,10 +2626,10 @@ requires-dist = [ { name = "azure-core-tracing-opentelemetry", specifier = ">=1.0.0b12" }, { name = "azure-cosmos", specifier = ">=4.9.0" }, { name = "azure-identity", specifier = ">=1.25.1" }, - { name = "azure-monitor-opentelemetry", specifier = ">=1.6.4" }, + { name = "azure-monitor-opentelemetry", specifier = ">=1.8.3" }, { name = "debugpy", specifier = ">=1.8.0" }, { name = "dotenv-azd", specifier = ">=0.1.0" }, - { name = "fastmcp", specifier = ">=2.13.3" }, + { name = "fastmcp", specifier = ">=2.14.2" }, { name = "langchain", specifier = "==1.0.0a5" }, { name = "langchain-core", specifier = ">=0.3.0" }, { name = "langchain-mcp-adapters", specifier = ">=0.1.11" }, @@ -2471,8 +2637,8 @@ requires-dist = [ { name = "logfire", specifier = ">=4.15.1" }, { name = "mcp", specifier = ">=1.3.0" }, { name = "msgraph-sdk", specifier = ">=1.0.0" }, - { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.28.0" }, - { name = "opentelemetry-instrumentation-starlette", specifier = ">=0.49b0" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.39.0" }, + { name = "opentelemetry-instrumentation-starlette", specifier = ">=0.60b0" }, ] [package.metadata.requires-dev] @@ -2518,6 +2684,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318 }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -2772,6 +2947,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215 }, ] +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + [[package]] name = "six" version = "1.17.0" @@ -2790,6 +2987,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, +] + [[package]] name = "sqlalchemy" version = "2.0.44" @@ -2891,6 +3097,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, ] +[[package]] +name = "typer" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381 }, +] + [[package]] name = "typing-extensions" version = "4.15.0" From b9d553c787b8c3d1ca02ac104c06bc89f8479ccf Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Wed, 7 Jan 2026 16:45:08 -0800 Subject: [PATCH 5/5] Address Copilot feedback --- README.md | 23 +++++++++++++---------- infra/auth_init.py | 2 +- servers/auth_entra_mcp.py | 12 ++++++++---- spanish/README.md | 20 ++++++++++---------- 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index fa26a23..ed66320 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ If you're not using one of the above options, then you'll need to: This project includes MCP servers in the [`servers/`](servers/) directory: | File | Description | -|------|-------------| +| ---- | ----------- | | [servers/basic_mcp_stdio.py](servers/basic_mcp_stdio.py) | MCP server with stdio transport for VS Code integration | | [servers/basic_mcp_http.py](servers/basic_mcp_http.py) | MCP server with HTTP transport on port 8000 | | [servers/deployed_mcp.py](servers/deployed_mcp.py) | MCP server for Azure deployment with Cosmos DB and optional Keycloak auth | @@ -200,7 +200,7 @@ You can use the [.NET Aspire Dashboard](https://learn.microsoft.com/dotnet/aspir This project includes example agents in the [`agents/`](agents/) directory that demonstrate how to connect AI agents to MCP servers: | File | Description | -|------|-------------| +| ---- | ----------- | | [agents/agentframework_learn.py](agents/agentframework_learn.py) | Microsoft Agent Framework integration with MCP | | [agents/agentframework_http.py](agents/agentframework_http.py) | Microsoft Agent Framework integration with local Expenses MCP server | | [agents/langchainv1_http.py](agents/langchainv1_http.py) | LangChain agent with MCP integration | @@ -380,10 +380,12 @@ When using VNet configuration, additional Azure resources are provisioned: This project supports deploying with OAuth 2.0 authentication using Keycloak as the identity provider, implementing the [MCP OAuth specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) with Dynamic Client Registration (DCR). +[📺 Watch a demo video of Keycloak integration](https://youtu.be/lpH8PI4JgEY) + ### What gets deployed | Component | Description | -|-----------|-------------| +| --------- | ----------- | | **Keycloak Container App** | Keycloak 26.0 with pre-configured realm | | **HTTP Route Configuration** | Rule-based routing: `/auth/*` → Keycloak, `/*` → MCP Server | | **OAuth-protected MCP Server** | FastMCP with JWT validation against Keycloak's JWKS endpoint | @@ -432,7 +434,6 @@ This project supports deploying with OAuth 2.0 authentication using Keycloak as Login with `admin` and your configured password. - ### Use Keycloak OAuth MCP server with GitHub Copilot The Keycloak deployment supports Dynamic Client Registration (DCR), which allows VS Code to automatically register as an OAuth client. VS Code redirect URIs are pre-configured in the Keycloak realm. @@ -476,7 +477,7 @@ To use the deployed MCP server with GitHub Copilot Chat: ### Known limitations (demo trade-offs) | Item | Current | Production Recommendation | Why | -|------|---------|---------------------------|-----| +| ---- | ------- | ------------------------- | --- | | Keycloak mode | `start-dev` | `start` with proper config | Dev mode has relaxed security defaults | | Database | H2 in-memory | PostgreSQL | H2 doesn't persist data across restarts | | Replicas | 1 (due to H2) | Multiple with shared DB | H2 is in-memory, can't share state | @@ -491,10 +492,12 @@ To use the deployed MCP server with GitHub Copilot Chat: This project supports deploying with Microsoft Entra ID (Azure AD) authentication using FastMCP's built-in Azure OAuth proxy. This is an alternative to Keycloak that uses Microsoft Entra with your Azure tenant for identity management. +[📺 Watch a demo video of Entra integration](https://youtu.be/nOPXUBOXU2M) + ### What gets deployed with Entra OAuth | Component | Description | -|-----------|-------------| +| --------- | ----------- | | **Microsoft Entra App Registration** | Created automatically during provisioning with redirect URIs for local development, VS Code, and production | | **OAuth-protected MCP Server** | FastMCP with AzureProvider for OAuth authentication | | **CosmosDB OAuth Client Storage** | Persists OAuth client registrations across server restarts | @@ -534,10 +537,10 @@ This project supports deploying with Microsoft Entra ID (Azure AD) authenticatio The following environment variables are automatically set by the deployment hooks: -| Variable | Description | -|----------|-------------| -| `ENTRA_PROXY_AZURE_CLIENT_ID` | The App Registration's client ID | -| `ENTRA_PROXY_AZURE_CLIENT_SECRET` | The App Registration's client secret | +| Variable | Description | +| ----------------------------------- | --------------------------------------- | +| `ENTRA_PROXY_AZURE_CLIENT_ID` | The App Registration's client ID | +| `ENTRA_PROXY_AZURE_CLIENT_SECRET` | The App Registration's client secret | These are then written to `.env` by the postprovision hook for local development. diff --git a/infra/auth_init.py b/infra/auth_init.py index 229edfa..3ed3b1d 100644 --- a/infra/auth_init.py +++ b/infra/auth_init.py @@ -258,7 +258,7 @@ async def main(): server_app_id = await create_or_update_fastmcp_app(graph_client) - print("Attempting to grant admin consent for the client and server applications...") + print("Attempting to grant admin consent for the server application...") await grant_application_admin_consent(graph_client, server_app_id) print("✅ Entra app registration setup is complete.") diff --git a/servers/auth_entra_mcp.py b/servers/auth_entra_mcp.py index 3fc838c..715ec12 100644 --- a/servers/auth_entra_mcp.py +++ b/servers/auth_entra_mcp.py @@ -262,7 +262,11 @@ async def get_expense_stats(ctx: Context): user_assertion=auth_token, scopes=["https://graph.microsoft.com/.default"] ) if "error" in graph_resource_access_token: - return "Error: Unable to obtain Graph API access token for authorization check" + logger.error( + "OBO token acquisition failed: %s", + graph_resource_access_token.get("error_description", "Unknown error"), + ) + return "Error: Unable to verify permissions. Please try again later." graph_auth_token = graph_resource_access_token["access_token"] @@ -292,9 +296,9 @@ async def get_expense_stats(ctx: Context): return summary - except Exception as e: - logger.error(f"Error retrieving expense stats: {str(e)}") - return f"Error: Unable to retrieve expense statistics - {str(e)}" + except Exception: + logger.error("Error retrieving expense stats", exc_info=True) + return "Error: Unable to retrieve expense statistics. Please try again later." @mcp.custom_route("/health", methods=["GET"]) diff --git a/spanish/README.md b/spanish/README.md index fb1e247..2d51972 100644 --- a/spanish/README.md +++ b/spanish/README.md @@ -16,7 +16,7 @@ Un proyecto de demostración que muestra implementaciones del Model Context Prot - [Correr Agentes <-> MCP](#correr-agentes---mcp) - [Desplegar en Azure](#desplegar-en-azure) - [Desplegar en Azure con red privada](#desplegar-en-azure-con-red-privada) -- [Desplegar en Azure con autenticación Keycloak](#desplegar-en-azure-con-autenticacion-keycloak) +- [Desplegar en Azure con autenticación Keycloak](#desplegar-en-azure-con-autenticación-keycloak) - [Desplegar en Azure con Entra OAuth Proxy](#desplegar-en-azure-con-entra-oauth-proxy) ## Empezar @@ -77,7 +77,7 @@ Si no usás una de las opciones anteriores, necesitás: Este proyecto incluye servidores MCP en el directorio [`servers/`](../servers/): | Archivo | Descripción | -|------|-------------| +| ------- | ----------- | | [servers/basic_mcp_stdio.py](../servers/basic_mcp_stdio.py) | Servidor MCP con transporte stdio para integración con VS Code | | [servers/basic_mcp_http.py](../servers/basic_mcp_http.py) | Servidor MCP con transporte HTTP en el puerto 8000 | | [servers/deployed_mcp.py](../servers/deployed_mcp.py) | Servidor MCP para despliegue en Azure con Cosmos DB y autenticación opcional con Keycloak | @@ -200,7 +200,7 @@ Podés usar el [.NET Aspire Dashboard](https://learn.microsoft.com/dotnet/aspire Este proyecto incluye agentes de ejemplo en el directorio [`agents/`](../agents/) que demuestran cómo conectar agentes de IA a servidores MCP: | Archivo | Descripción | -|------|-------------| +| ------- | ----------- | | [agents/agentframework_learn.py](../agents/agentframework_learn.py) | Integración del Microsoft Agent Framework con MCP | | [agents/agentframework_http.py](../agents/agentframework_http.py) | Integración del Microsoft Agent Framework con el servidor MCP de gastos local | | [agents/langchainv1_http.py](../agents/langchainv1_http.py) | Agente LangChain con integración MCP | @@ -339,7 +339,7 @@ Este proyecto soporta desplegar con autenticación OAuth 2.0 usando Keycloak com ### Qué se despliega | Componente | Descripción | -|-----------|-------------| +| ---------- | ----------- | | **Container App de Keycloak** | Keycloak 26.0 con realm preconfigurado | | **Configuración de rutas HTTP** | Enrutamiento basado en reglas: `/auth/*` → Keycloak, `/*` → Servidor MCP | | **Servidor MCP protegido con OAuth** | FastMCP con validación JWT contra el endpoint JWKS de Keycloak | @@ -449,7 +449,7 @@ Para usar el servidor MCP desplegado con GitHub Copilot Chat: ### Limitaciones conocidas (trade-offs de la demo) | Ítem | Actual | Recomendación para producción | Por qué | -|------|--------|-------------------------------|--------| +| ---- | ------ | ----------------------------- | ------- | | Modo de Keycloak | `start-dev` | `start` con configuración adecuada | El modo dev tiene defaults de seguridad relajados | | Base de datos | H2 en memoria | PostgreSQL | H2 no persiste datos entre reinicios | | Réplicas | 1 (por H2) | Múltiples con DB compartida | H2 es en memoria, no comparte estado | @@ -467,7 +467,7 @@ Este proyecto soporta desplegar con Microsoft Entra ID (Azure AD) usando el prox ### Qué se despliega con Entra OAuth | Componente | Descripción | -|-----------|-------------| +| ---------- | ----------- | | **App Registration de Microsoft Entra** | Se crea automáticamente durante el aprovisionamiento con URIs de redirección para desarrollo local, VS Code y producción | | **Servidor MCP protegido con OAuth** | FastMCP con AzureProvider para autenticación OAuth | | **Almacenamiento de clientes OAuth en CosmosDB** | Persiste registros de clientes OAuth entre reinicios del servidor | @@ -507,10 +507,10 @@ Este proyecto soporta desplegar con Microsoft Entra ID (Azure AD) usando el prox Las siguientes variables de entorno se definen automáticamente por los hooks de despliegue: -| Variable | Descripción | -|----------|-------------| -| `ENTRA_PROXY_AZURE_CLIENT_ID` | ID de cliente de la App Registration | -| `ENTRA_PROXY_AZURE_CLIENT_SECRET` | Secreto de cliente de la App Registration | +| Variable | Descripción | +| ----------------------------------- | ----------------------------------------- | +| `ENTRA_PROXY_AZURE_CLIENT_ID` | ID de cliente de la App Registration | +| `ENTRA_PROXY_AZURE_CLIENT_SECRET` | Secreto de cliente de la App Registration | Estas luego se escriben a `.env` por el hook de postprovision para desarrollo local.