diff --git a/.github/ISSUE_TEMPLATE/GENERAL.md b/.github/ISSUE_TEMPLATE/GENERAL.md index 991da02..84cea1c 100644 --- a/.github/ISSUE_TEMPLATE/GENERAL.md +++ b/.github/ISSUE_TEMPLATE/GENERAL.md @@ -6,6 +6,7 @@ labels: '' assignees: '' --- + diff --git a/.github/ISSUE_TEMPLATE/user-story---tasks.md b/.github/ISSUE_TEMPLATE/user-story---tasks.md new file mode 100644 index 0000000..f8b8a07 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/user-story---tasks.md @@ -0,0 +1,16 @@ +--- +name: user story & tasks +about: user story 및 하위 태스크 작성 +title: 'Onedrive-download/User Story 1.2:' +labels: '' +assignees: '' + +--- + +### 📬 User Story 2 (main title of user story) +#### 개발자로서, ~~ +_부연 설명 및 참고 사항_ + +--- +* **Tasks** + - [ ] task 내용 diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 3bd62f4..920d570 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,32 +1,8 @@ { "servers": { - "awesome-copilot": { - "type": "stdio", - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "ghcr.io/microsoft/mcp-dotnet-samples/awesome-copilot:latest" - ] - }, - "microsoft.docs.mcp": { + "ppt-font-fix": { "type": "http", - "url": "https://learn.microsoft.com/api/mcp" - }, - "github": { - "type": "http", - "url": "https://api.githubcopilot.com/mcp/" - }, - "sequential-thinking": { - "type": "stdio", - "command": "docker", - "args": [ - "run", - "--rm", - "-i", - "mcp/sequentialthinking" - ] + "url": "http://localhost:8080/mcp" } } -} \ No newline at end of file +} diff --git a/Dockerfile.ppt-font-fix b/Dockerfile.ppt-font-fix new file mode 100644 index 0000000..2320864 --- /dev/null +++ b/Dockerfile.ppt-font-fix @@ -0,0 +1,32 @@ +# syntax=docker/dockerfile:1 + +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build + +COPY ./shared/McpSamples.Shared /source/shared/McpSamples.Shared +COPY ./ppt-font-fix/src/McpSamples.PptFontFix.HybridApp /source/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp + +WORKDIR /source/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp + +ARG TARGETARCH +RUN case "$TARGETARCH" in \ + "amd64") RID="linux-musl-x64" ;; \ + "arm64") RID="linux-musl-arm64" ;; \ + *) RID="linux-musl-x64" ;; \ + esac && \ + dotnet publish -c Release -o /app -r $RID --self-contained false + +FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final + +WORKDIR /app + +COPY --from=build /app . + +RUN mkdir -p /app/wwwroot/generated /app/workspace && \ + mkdir -p /files/input && \ + mkdir -p /files/output && \ + chown -R $APP_UID:$APP_UID /app/wwwroot/generated /app/workspace && \ + chmod 755 /app/wwwroot/generated + +USER $APP_UID + +ENTRYPOINT ["dotnet", "McpSamples.PptFontFix.HybridApp.dll"] \ No newline at end of file diff --git a/Dockerfile.ppt-font-fix-azure b/Dockerfile.ppt-font-fix-azure new file mode 100644 index 0000000..68ab0ef --- /dev/null +++ b/Dockerfile.ppt-font-fix-azure @@ -0,0 +1,30 @@ +# syntax=docker/dockerfile:1 + +FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build + +COPY ./shared/McpSamples.Shared /source/shared/McpSamples.Shared +COPY ./ppt-font-fix/src/McpSamples.PptFontFix.HybridApp /source/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp + +WORKDIR /source/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp + +RUN dotnet publish -c Release -o /app --self-contained false + +FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final + +WORKDIR /app + +COPY --from=build /app . + +RUN mkdir -p /app/wwwroot/generated \ + && mkdir -p /app/workspace \ + && mkdir -p /files + +ARG UID=$APP_UID + +RUN chown -R $UID:$UID /app/wwwroot/generated \ + && chown -R $UID:$UID /app/workspace \ + && chown -R $UID:$UID /files + +USER $UID + +ENTRYPOINT ["dotnet", "McpSamples.PptFontFix.HybridApp.dll"] \ No newline at end of file diff --git a/ppt-font-fix/.gitignore b/ppt-font-fix/.gitignore new file mode 100644 index 0000000..8483aa7 --- /dev/null +++ b/ppt-font-fix/.gitignore @@ -0,0 +1,2 @@ +.vscode/mcp.json +.azure \ No newline at end of file diff --git a/ppt-font-fix/.vscode/mcp.http.container.json b/ppt-font-fix/.vscode/mcp.http.container.json new file mode 100644 index 0000000..920d570 --- /dev/null +++ b/ppt-font-fix/.vscode/mcp.http.container.json @@ -0,0 +1,8 @@ +{ + "servers": { + "ppt-font-fix": { + "type": "http", + "url": "http://localhost:8080/mcp" + } + } +} diff --git a/ppt-font-fix/.vscode/mcp.http.local.json b/ppt-font-fix/.vscode/mcp.http.local.json new file mode 100644 index 0000000..a6e57a2 --- /dev/null +++ b/ppt-font-fix/.vscode/mcp.http.local.json @@ -0,0 +1,8 @@ +{ + "servers": { + "ppt-font-fix": { + "type": "http", + "url": "http://localhost:5166/mcp" + } + } +} diff --git a/ppt-font-fix/.vscode/mcp.http.remote.json b/ppt-font-fix/.vscode/mcp.http.remote.json new file mode 100644 index 0000000..497a479 --- /dev/null +++ b/ppt-font-fix/.vscode/mcp.http.remote.json @@ -0,0 +1,15 @@ +{ + "inputs": [ + { + "type": "promptString", + "id": "acaapp-server-fqdn", + "description": "Azure Container Apps FQDN" + } + ], + "servers": { + "ppt-font-fix": { + "type": "http", + "url": "https://${input:acaapp-server-fqdn}/mcp" + } + } +} diff --git a/ppt-font-fix/.vscode/mcp.stdio.container.json b/ppt-font-fix/.vscode/mcp.stdio.container.json new file mode 100644 index 0000000..7b6d76f --- /dev/null +++ b/ppt-font-fix/.vscode/mcp.stdio.container.json @@ -0,0 +1,22 @@ +{ + "inputs": [ + { + "type": "promptString", + "id": "your-ppt-file-path", + "description": "The absolute path where the PPT file you want to modify is located" + } + ], + "servers": { + "ppt-font-fix": { + "type": "stdio", + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-v", "${input:your-ppt-file-path}:/files", + "ppt-font-fix:latest" + ] + } + } +} \ No newline at end of file diff --git a/ppt-font-fix/.vscode/mcp.stdio.local.json b/ppt-font-fix/.vscode/mcp.stdio.local.json new file mode 100644 index 0000000..98daeec --- /dev/null +++ b/ppt-font-fix/.vscode/mcp.stdio.local.json @@ -0,0 +1,20 @@ +{ + "inputs": [ + { + "type": "promptString", + "id": "consoleapp-project-path", + "description": "The absolute path to the console app project Directory" + } + ], + "servers": { + "ppt-font-fix": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "${input:consoleapp-project-path}" + ] + } + } +} diff --git a/ppt-font-fix/McpPptFontFix.sln b/ppt-font-fix/McpPptFontFix.sln new file mode 100644 index 0000000..6e2e1de --- /dev/null +++ b/ppt-font-fix/McpPptFontFix.sln @@ -0,0 +1,53 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "McpSamples.PptFontFix.HybridApp", "src\McpSamples.PptFontFix.HybridApp\McpSamples.PptFontFix.HybridApp.csproj", "{4E3C401C-B16C-459D-8AFC-896CCF5DBACF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "McpSamples.Shared", "..\shared\McpSamples.Shared\McpSamples.Shared.csproj", "{1F4DA2CA-2377-4800-A275-02F65E68CC2D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4E3C401C-B16C-459D-8AFC-896CCF5DBACF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E3C401C-B16C-459D-8AFC-896CCF5DBACF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E3C401C-B16C-459D-8AFC-896CCF5DBACF}.Debug|x64.ActiveCfg = Debug|Any CPU + {4E3C401C-B16C-459D-8AFC-896CCF5DBACF}.Debug|x64.Build.0 = Debug|Any CPU + {4E3C401C-B16C-459D-8AFC-896CCF5DBACF}.Debug|x86.ActiveCfg = Debug|Any CPU + {4E3C401C-B16C-459D-8AFC-896CCF5DBACF}.Debug|x86.Build.0 = Debug|Any CPU + {4E3C401C-B16C-459D-8AFC-896CCF5DBACF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E3C401C-B16C-459D-8AFC-896CCF5DBACF}.Release|Any CPU.Build.0 = Release|Any CPU + {4E3C401C-B16C-459D-8AFC-896CCF5DBACF}.Release|x64.ActiveCfg = Release|Any CPU + {4E3C401C-B16C-459D-8AFC-896CCF5DBACF}.Release|x64.Build.0 = Release|Any CPU + {4E3C401C-B16C-459D-8AFC-896CCF5DBACF}.Release|x86.ActiveCfg = Release|Any CPU + {4E3C401C-B16C-459D-8AFC-896CCF5DBACF}.Release|x86.Build.0 = Release|Any CPU + {1F4DA2CA-2377-4800-A275-02F65E68CC2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F4DA2CA-2377-4800-A275-02F65E68CC2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F4DA2CA-2377-4800-A275-02F65E68CC2D}.Debug|x64.ActiveCfg = Debug|Any CPU + {1F4DA2CA-2377-4800-A275-02F65E68CC2D}.Debug|x64.Build.0 = Debug|Any CPU + {1F4DA2CA-2377-4800-A275-02F65E68CC2D}.Debug|x86.ActiveCfg = Debug|Any CPU + {1F4DA2CA-2377-4800-A275-02F65E68CC2D}.Debug|x86.Build.0 = Debug|Any CPU + {1F4DA2CA-2377-4800-A275-02F65E68CC2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F4DA2CA-2377-4800-A275-02F65E68CC2D}.Release|Any CPU.Build.0 = Release|Any CPU + {1F4DA2CA-2377-4800-A275-02F65E68CC2D}.Release|x64.ActiveCfg = Release|Any CPU + {1F4DA2CA-2377-4800-A275-02F65E68CC2D}.Release|x64.Build.0 = Release|Any CPU + {1F4DA2CA-2377-4800-A275-02F65E68CC2D}.Release|x86.ActiveCfg = Release|Any CPU + {1F4DA2CA-2377-4800-A275-02F65E68CC2D}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {4E3C401C-B16C-459D-8AFC-896CCF5DBACF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + EndGlobalSection +EndGlobal diff --git a/ppt-font-fix/README.md b/ppt-font-fix/README.md new file mode 100644 index 0000000..3e5ec79 --- /dev/null +++ b/ppt-font-fix/README.md @@ -0,0 +1,234 @@ +# MCP Server: PPT Font Fix + +This is an MCP server that finds fonts in a .pptx file, analyzes their usages - used, unused, inconsistently used, and fixes it. + +## Install + +[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)]() [![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)]() + +## Prerequisites + +- [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) +- [Visual Studio Code](https://code.visualstudio.com/) with + - [C# Dev Kit](https://marketplace.visualstudio.com/items/?itemName=ms-dotnettools.csdevkit) extension +- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) +- [Azure Developer CLI](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd) +- [Docker Desktop](https://docs.docker.com/get-started/get-docker/) + +## What's Included + +PPT Font Fix MCP server includes: + +| Building Block | Name | Description | Usage | +|----------------|----------------------------|-------------------------------------|-----------------------------| +| Tools          | `open_ppt_file`         | Opens a PPTX file.       | `#open_ppt_file` | +| Tools          | `analyze_ppt_file`         | Analyzes fonts used in a PPTX file.       | `#analyze_ppt_file` | +| Tools          | `update_ppt_file`          | Replaces inconsistent fonts and saves the modified file.     | `#update_ppt_file` | +| Prompts        | `fix_ppt_fonts`            | Structured workflow to guide file upload and repair process. | `/mcp.ppt-font-fix.fix_ppt_fonts ` | + +## Getting Started + +- [Getting repository root](#getting-repository-root) +- [Running MCP server](#running-mcp-server) + - [On a local machine](#on-a-local-machine) + - [In a container](#in-a-container) + - [On Azure](#on-azure) +- [Connect MCP server to an MCP host/client](#connect-mcp-server-to-an-mcp-hostclient) + - [VS Code + Agent Mode + Local MCP server](#vs-code--agent-mode--local-mcp-server) + +### Getting repository root + +1. Get the repository root. + + ```bash + # bash/zsh + REPOSITORY_ROOT=$(git rev-parse --show-toplevel) + ``` + + ```powershell + # PowerShell + $REPOSITORY_ROOT = git rev-parse --show-toplevel + ``` + +### Running MCP server + +#### On a local machine + +1. Run the MCP server app. + + ```bash + cd $REPOSITORY_ROOT/ppt-font-fix + dotnet run --project ./src/McpSamples.PptFontFix.HybridApp + ``` + + > Make sure take note the absolute directory path of the `McpSamples.PptFontFix.HybridApp` project. + + **Parameters**: + + - `--http`: The switch that indicates to run this MCP server as a streamable HTTP type. When this switch is added, the MCP server URL is `http://localhost:5166`. + + With these parameters, you can run the MCP server like: + + ```bash + dotnet run --project ./src/McpSamples.PptFontFix.HybridApp -- --http + ``` + +#### In a container + +1. Build the MCP server app as a container image. + + ```bash + cd $REPOSITORY_ROOT + docker build -f Dockerfile.ppt-font-fix -t ppt-font-fix:latest . + ``` + > Make sure take note the absolute directory path of the `ppt-font-fix` project. + +1. Run the MCP server app in a container. + + ```bash + docker run -i --rm -v "$REPOSITORY_ROOT/ppt-font-fix/workspace:/files" ppt-font-fix:latest -c + ``` + + Alternatively, use the container image from the container registry. + + ```bash + docker run -i --rm -p 8080:8080 -v "$REPOSITORY_ROOT/ppt-font-fix/workspace:/files" ghcr.io/microsoft/mcp-dotnet-samples/ppt-font-fix:latest -c + ``` + + **Parameters**: + + - `--http`: The switch that indicates to run this MCP server as a streamable HTTP type. When this switch is added, the MCP server URL is `http://localhost:8080`. + + With these parameters, you can run the MCP server like: + + ```bash + # use local container image + docker run -it --rm -p 8080:8080 -u 0 -v "$REPOSITORY_ROOT/ppt-font-fix/workspace:/files" ppt-font-fix:latest --http -c + ``` + + ```bash + # use container image from the container registry + docker run -it --rm -p 8080:8080 -u 0 -v "$REPOSITORY_ROOT/ppt-font-fix/workspace:/files" ghcr.io/microsoft/mcp-dotnet-samples/ppt-font-fix:latest --http -c + ``` + +#### On Azure + +1. Navigate to the directory. + + ```bash + cd $REPOSITORY_ROOT/ppt-font-fix + ``` + +1. Login to Azure. + + ```bash + # Login with Azure Developer CLI + azd auth login + ``` + +1. Deploy the MCP server app to Azure. + + ```bash + azd up + ``` + + While provisioning and deploying, you'll be asked to provide subscription ID, location, environment name. + +1. After the deployment is complete, get the information by running the following commands: + + - Azure Container Apps FQDN: + + ```bash + azd env get-value AZURE_RESOURCE_MCP_PPT_FONT_FIX_FQDN + ``` + + If you want to use Azure, you must copy the account key or upload the file to the blob storage on the running server, and copy the URL of the uploaded file. + +### Connect MCP server to an MCP host/client + +#### VS Code + Agent Mode + Local MCP server + +1. Copy `mcp.json` to the repository root. + + **For locally running MCP server (STDIO):** + + ```bash + mkdir -p $REPOSITORY_ROOT/.vscode + cp $REPOSITORY_ROOT/ppt-font-fix/.vscode/mcp.stdio.local.json \ + $REPOSITORY_ROOT/.vscode/mcp.json + ``` + + ```powershell + New-Item -Type Directory -Path $REPOSITORY_ROOT/.vscode -Force + Copy-Item -Path $REPOSITORY_ROOT/ppt-font-fix/.vscode/mcp.stdio.local.json ` + -Destination $REPOSITORY_ROOT/.vscode/mcp.json -Force + ``` + + **For locally running MCP server (HTTP):** + + ```bash + mkdir -p $REPOSITORY_ROOT/.vscode + cp $REPOSITORY_ROOT/ppt-font-fix/.vscode/mcp.http.local.json \ + $REPOSITORY_ROOT/.vscode/mcp.json + ``` + + ```powershell + New-Item -Type Directory -Path $REPOSITORY_ROOT/.vscode -Force + Copy-Item -Path $REPOSITORY_ROOT/ppt-font-fix/.vscode/mcp.http.local.json ` + -Destination $REPOSITORY_ROOT/.vscode/mcp.json -Force + ``` + + **For locally running MCP server in a container (STDIO):** + + ```bash + mkdir -p $REPOSITORY_ROOT/.vscode + cp $REPOSITORY_ROOT/ppt-font-fix/.vscode/mcp.stdio.container.json \ + $REPOSITORY_ROOT/.vscode/mcp.json + ``` + + ```powershell + New-Item -Type Directory -Path $REPOSITORY_ROOT/.vscode -Force + Copy-Item -Path $REPOSITORY_ROOT/ppt-font-fix/.vscode/mcp.stdio.container.json ` + -Destination $REPOSITORY_ROOT/.vscode/mcp.json -Force + ``` + + **For locally running MCP server in a container (HTTP):** + + ```bash + mkdir -p $REPOSITORY_ROOT/.vscode + cp $REPOSITORY_ROOT/ppt-font-fix/.vscode/mcp.http.container.json \ + $REPOSITORY_ROOT/.vscode/mcp.json + ``` + + ```powershell + New-Item -Type Directory -Path $REPOSITORY_ROOT/.vscode -Force + Copy-Item -Path $REPOSITORY_ROOT/ppt-font-fix/.vscode/mcp.http.container.json ` + -Destination $REPOSITORY_ROOT/.vscode/mcp.json -Force + ``` + + **For remotely running MCP server in a container (HTTP):** + + ```bash + mkdir -p $REPOSITORY_ROOT/.vscode + cp $REPOSITORY_ROOT/ppt-font-fix/.vscode/mcp.http.remote.json \ + $REPOSITORY_ROOT/.vscode/mcp.json + ``` + + ```powershell + New-Item -Type Directory -Path $REPOSITORY_ROOT/.vscode -Force + Copy-Item -Path $REPOSITORY_ROOT/ppt-font-fix/.vscode/mcp.http.remote.json ` + -Destination $REPOSITORY_ROOT/.vscode/mcp.json -Force + ``` + +1. Open Command Palette by typing `F1` or `Ctrl`+`Shift`+`P` on Windows or `Cmd`+`Shift`+`P` on Mac OS, and search `MCP: List Servers`. +1. Choose `ppt-font-fix` then click `Start Server`. +1. When prompted, enter one of the following values: + - The absolute directory path of the `McpSamples.PptFontFix.HybridApp` project + - The FQDN of Azure Container Apps. +1. Enter prompt like: + + ```text + Analyze and Fix the fonts in C:\path\to\presentation.pptx + ``` + +1. Confirm the result. diff --git a/ppt-font-fix/azure.yaml b/ppt-font-fix/azure.yaml new file mode 100644 index 0000000..ce91d9e --- /dev/null +++ b/ppt-font-fix/azure.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: ppt-font-fix + +metadata: + template: azd-init@1.14.0 + +services: + ppt-font-fix: + project: src/McpSamples.PptFontFix.HybridApp + host: containerapp + language: dotnet + docker: + path: ../../../Dockerfile.ppt-font-fix-azure + context: ../../../ + remoteBuild: true diff --git a/ppt-font-fix/infra/abbreviations.json b/ppt-font-fix/infra/abbreviations.json new file mode 100644 index 0000000..1533dee --- /dev/null +++ b/ppt-font-fix/infra/abbreviations.json @@ -0,0 +1,136 @@ +{ + "analysisServicesServers": "as", + "apiManagementService": "apim-", + "appConfigurationStores": "appcs-", + "appManagedEnvironments": "cae-", + "appContainerApps": "ca-", + "authorizationPolicyDefinitions": "policy-", + "automationAutomationAccounts": "aa-", + "blueprintBlueprints": "bp-", + "blueprintBlueprintsArtifacts": "bpa-", + "cacheRedis": "redis-", + "cdnProfiles": "cdnp-", + "cdnProfilesEndpoints": "cdne-", + "cognitiveServicesAccounts": "cog-", + "cognitiveServicesFormRecognizer": "cog-fr-", + "cognitiveServicesTextAnalytics": "cog-ta-", + "computeAvailabilitySets": "avail-", + "computeCloudServices": "cld-", + "computeDiskEncryptionSets": "des", + "computeDisks": "disk", + "computeDisksOs": "osdisk", + "computeGalleries": "gal", + "computeSnapshots": "snap-", + "computeVirtualMachines": "vm", + "computeVirtualMachineScaleSets": "vmss-", + "containerInstanceContainerGroups": "ci", + "containerRegistryRegistries": "cr", + "containerServiceManagedClusters": "aks-", + "databricksWorkspaces": "dbw-", + "dataFactoryFactories": "adf-", + "dataLakeAnalyticsAccounts": "dla", + "dataLakeStoreAccounts": "dls", + "dataMigrationServices": "dms-", + "dBforMySQLServers": "mysql-", + "dBforPostgreSQLServers": "psql-", + "devicesIotHubs": "iot-", + "devicesProvisioningServices": "provs-", + "devicesProvisioningServicesCertificates": "pcert-", + "documentDBDatabaseAccounts": "cosmos-", + "documentDBMongoDatabaseAccounts": "cosmon-", + "eventGridDomains": "evgd-", + "eventGridDomainsTopics": "evgt-", + "eventGridEventSubscriptions": "evgs-", + "eventHubNamespaces": "evhns-", + "eventHubNamespacesEventHubs": "evh-", + "hdInsightClustersHadoop": "hadoop-", + "hdInsightClustersHbase": "hbase-", + "hdInsightClustersKafka": "kafka-", + "hdInsightClustersMl": "mls-", + "hdInsightClustersSpark": "spark-", + "hdInsightClustersStorm": "storm-", + "hybridComputeMachines": "arcs-", + "insightsActionGroups": "ag-", + "insightsComponents": "appi-", + "keyVaultVaults": "kv-", + "kubernetesConnectedClusters": "arck", + "kustoClusters": "dec", + "kustoClustersDatabases": "dedb", + "logicIntegrationAccounts": "ia-", + "logicWorkflows": "logic-", + "machineLearningServicesWorkspaces": "mlw-", + "managedIdentityUserAssignedIdentities": "id-", + "managementManagementGroups": "mg-", + "migrateAssessmentProjects": "migr-", + "networkApplicationGateways": "agw-", + "networkApplicationSecurityGroups": "asg-", + "networkAzureFirewalls": "afw-", + "networkBastionHosts": "bas-", + "networkConnections": "con-", + "networkDnsZones": "dnsz-", + "networkExpressRouteCircuits": "erc-", + "networkFirewallPolicies": "afwp-", + "networkFirewallPoliciesWebApplication": "waf", + "networkFirewallPoliciesRuleGroups": "wafrg", + "networkFrontDoors": "fd-", + "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", + "networkLoadBalancersExternal": "lbe-", + "networkLoadBalancersInternal": "lbi-", + "networkLoadBalancersInboundNatRules": "rule-", + "networkLocalNetworkGateways": "lgw-", + "networkNatGateways": "ng-", + "networkNetworkInterfaces": "nic-", + "networkNetworkSecurityGroups": "nsg-", + "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", + "networkNetworkWatchers": "nw-", + "networkPrivateDnsZones": "pdnsz-", + "networkPrivateLinkServices": "pl-", + "networkPublicIPAddresses": "pip-", + "networkPublicIPPrefixes": "ippre-", + "networkRouteFilters": "rf-", + "networkRouteTables": "rt-", + "networkRouteTablesRoutes": "udr-", + "networkTrafficManagerProfiles": "traf-", + "networkVirtualNetworkGateways": "vgw-", + "networkVirtualNetworks": "vnet-", + "networkVirtualNetworksSubnets": "snet-", + "networkVirtualNetworksVirtualNetworkPeerings": "peer-", + "networkVirtualWans": "vwan-", + "networkVpnGateways": "vpng-", + "networkVpnGatewaysVpnConnections": "vcn-", + "networkVpnGatewaysVpnSites": "vst-", + "notificationHubsNamespaces": "ntfns-", + "notificationHubsNamespacesNotificationHubs": "ntf-", + "operationalInsightsWorkspaces": "log-", + "portalDashboards": "dash-", + "powerBIDedicatedCapacities": "pbi-", + "purviewAccounts": "pview-", + "recoveryServicesVaults": "rsv-", + "resourcesResourceGroups": "rg-", + "searchSearchServices": "srch-", + "serviceBusNamespaces": "sb-", + "serviceBusNamespacesQueues": "sbq-", + "serviceBusNamespacesTopics": "sbt-", + "serviceEndPointPolicies": "se-", + "serviceFabricClusters": "sf-", + "signalRServiceSignalR": "sigr", + "sqlManagedInstances": "sqlmi-", + "sqlServers": "sql-", + "sqlServersDataWarehouse": "sqldw-", + "sqlServersDatabases": "sqldb-", + "sqlServersDatabasesStretch": "sqlstrdb-", + "storageStorageAccounts": "st", + "storageStorageAccountsVm": "stvm", + "storSimpleManagers": "ssimp", + "streamAnalyticsCluster": "asa-", + "synapseWorkspaces": "syn", + "synapseWorkspacesAnalyticsWorkspaces": "synw", + "synapseWorkspacesSqlPoolsDedicated": "syndp", + "synapseWorkspacesSqlPoolsSpark": "synsp", + "timeSeriesInsightsEnvironments": "tsi-", + "webServerFarms": "plan-", + "webSitesAppService": "app-", + "webSitesAppServiceEnvironment": "ase-", + "webSitesFunctions": "func-", + "webStaticSites": "stapp-" +} diff --git a/ppt-font-fix/infra/main.bicep b/ppt-font-fix/infra/main.bicep new file mode 100644 index 0000000..2a4e83b --- /dev/null +++ b/ppt-font-fix/infra/main.bicep @@ -0,0 +1,50 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(64) +@description('Name of the environment that can be used as part of naming resource convention') +param environmentName string + +@minLength(1) +@description('Primary location for all resources') +param location string + +param mcpPptFontFixExists bool + +@description('Id of the user or app to assign application roles') +param principalId string + +// Tags that should be applied to all resources. +// +// Note that 'azd-service-name' tags should be applied separately to service host resources. +// Example usage: +// tags: union(tags, { 'azd-service-name': }) +var tags = { + 'azd-env-name': environmentName +} + +// Organize resources in a resource group +resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: 'rg-${environmentName}' + location: location + tags: tags +} + +module resources 'resources.bicep' = { + scope: rg + name: 'resources' + params: { + location: location + tags: tags + principalId: principalId + mcpPptFontFixExists: mcpPptFontFixExists + } +} + +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT +output AZURE_RESOURCE_MCP_PPT_FONT_FIX_ID string = resources.outputs.AZURE_RESOURCE_MCP_PPT_FONT_FIX_ID +output AZURE_RESOURCE_MCP_PPT_FONT_FIX_NAME string = resources.outputs.AZURE_RESOURCE_MCP_PPT_FONT_FIX_NAME +output AZURE_RESOURCE_MCP_PPT_FONT_FIX_FQDN string = resources.outputs.AZURE_RESOURCE_MCP_PPT_FONT_FIX_FQDN +output AZURE_STORAGE_ACCOUNT_NAME string = resources.outputs.AZURE_STORAGE_ACCOUNT_NAME +output AZURE_FILE_SHARE_NAME string = resources.outputs.AZURE_FILE_SHARE_NAME +output AZURE_BLOB_CONTAINER_NAME string = resources.outputs.AZURE_BLOB_CONTAINER_NAME diff --git a/ppt-font-fix/infra/main.parameters.json b/ppt-font-fix/infra/main.parameters.json new file mode 100644 index 0000000..259447e --- /dev/null +++ b/ppt-font-fix/infra/main.parameters.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "mcpPptFontFixExists": { + "value": "${SERVICE_MCP_PPT_FONT_FIX_RESOURCE_EXISTS=false}" + }, + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + } + } +} diff --git a/ppt-font-fix/infra/modules/fetch-container-image.bicep b/ppt-font-fix/infra/modules/fetch-container-image.bicep new file mode 100644 index 0000000..78d1e7e --- /dev/null +++ b/ppt-font-fix/infra/modules/fetch-container-image.bicep @@ -0,0 +1,8 @@ +param exists bool +param name string + +resource existingApp 'Microsoft.App/containerApps@2023-05-02-preview' existing = if (exists) { + name: name +} + +output containers array = exists ? existingApp.properties.template.containers : [] diff --git a/ppt-font-fix/infra/resources.bicep b/ppt-font-fix/infra/resources.bicep new file mode 100644 index 0000000..0ccc3cb --- /dev/null +++ b/ppt-font-fix/infra/resources.bicep @@ -0,0 +1,259 @@ +@description('The location used for all deployed resources') +param location string = resourceGroup().location + +@description('Tags that will be applied to all resources') +param tags object = {} + +param mcpPptFontFixExists bool + +@description('Id of the user or app to assign application roles') +param principalId string + +var containerAppName = 'ppt-font-fix' + +var abbrs = loadJsonContent('./abbreviations.json') +var resourceToken = uniqueString(subscription().id, resourceGroup().id, location) +var storageKey = storage.listKeys().keys[0].value +var storageConnectionString = 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storageKey};EndpointSuffix=core.windows.net' + + +// Monitor application with Azure Monitor +module monitoring 'br/public:avm/ptn/azd/monitoring:0.1.0' = { + name: 'monitoring' + params: { + logAnalyticsName: '${abbrs.operationalInsightsWorkspaces}${resourceToken}' + applicationInsightsName: '${abbrs.insightsComponents}${resourceToken}' + applicationInsightsDashboardName: '${abbrs.portalDashboards}${resourceToken}' + location: location + tags: tags + } +} + +// Storage +resource storage 'Microsoft.Storage/storageAccounts@2022-09-01' = { + name: 'st${resourceToken}' + location: location + kind: 'StorageV2' + tags: tags + sku: { + name: 'Standard_LRS' + } + properties: { + largeFileSharesState: 'Enabled' + } +} + +resource storageBlobService 'Microsoft.Storage/storageAccounts/blobServices@2022-09-01' = { + parent: storage + name: 'default' +} + +resource generatedFilesContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' = { + parent: storageBlobService + name: 'generated-files' + properties: { + publicAccess: 'None' + } +} + +resource storageFileService 'Microsoft.Storage/storageAccounts/fileServices@2022-09-01' = { + parent: storage + name: 'default' +} + +resource storageFileShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2022-09-01' = { + parent: storageFileService + name: 'ppt-files' + properties: { + shareQuota: 1024 + enabledProtocols: 'SMB' + } +} + +// Container Registry +module containerRegistry 'br/public:avm/res/container-registry/registry:0.1.1' = { + name: 'registry' + params: { + name: '${abbrs.containerRegistryRegistries}${resourceToken}' + location: location + tags: tags + publicNetworkAccess: 'Enabled' + roleAssignments: [ + { + principalId: mcpPptFontFixIdentity.outputs.principalId + principalType: 'ServicePrincipal' + // ACR pull role + roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + } + ] + } +} + +// Container apps environment +module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.11.3' = { + name: 'container-apps-environment' + params: { + appInsightsConnectionString: monitoring.outputs.applicationInsightsConnectionString + name: '${abbrs.appManagedEnvironments}${resourceToken}' + location: location + zoneRedundant: false + publicNetworkAccess: 'Enabled' + + workloadProfiles: [ + { + workloadProfileType: 'Consumption' + name: 'Consumption' + } + ] + } +} + +resource existingEnv 'Microsoft.App/managedEnvironments@2023-05-01' existing = { + name: '${abbrs.appManagedEnvironments}${resourceToken}' +} + +resource explicitStorageLink 'Microsoft.App/managedEnvironments/storages@2023-05-01' = { + parent: existingEnv + name: 'ppt-storage-link' + properties: { + azureFile: { + accountName: storage.name + shareName: storageFileShare.name + accessMode: 'ReadWrite' + accountKey: storage.listKeys().keys[0].value + } + } + dependsOn: [ + containerAppsEnvironment + ] +} + +module mcpPptFontFixIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { + name: 'mcpPptFontFixIdentity' + params: { + name: '${abbrs.managedIdentityUserAssignedIdentities}mcpPptFontFix-${resourceToken}' + location: location + } +} + +// Azure Container Apps +module mcpPptFontFixFetchLatestImage './modules/fetch-container-image.bicep' = { + name: 'mcpPptFontFix-fetch-image' + params: { + exists: mcpPptFontFixExists + name: 'ppt-font-fix' + } +} + +module mcpPptFontFix 'br/public:avm/res/app/container-app:0.8.0' = { + name: 'mcpPptFontFix' + + dependsOn: [ + explicitStorageLink + storage + ] + + params: { + name: containerAppName + ingressTargetPort: 8080 + scaleMinReplicas: 1 + scaleMaxReplicas: 10 + secrets: { + secureList: [ + ] + } + volumes: [ + { + name: 'ppt-files-volume' + storageType: 'AzureFile' + storageName: 'ppt-storage-link' + } + ] + containers: [ + { + image: mcpPptFontFixFetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' + name: 'main' + resources: { + cpu: json('0.5') + memory: '1.0Gi' + } + env: [ + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: monitoring.outputs.applicationInsightsConnectionString + } + { + name: 'AZURE_CLIENT_ID' + value: mcpPptFontFixIdentity.outputs.clientId + } + { + name: 'PORT' + value: '8080' + } + { + name: 'AzureBlobConnectionString' + value: storageConnectionString + } + { + name: 'AZURE_FILE_SHARE_MOUNT_PATH' + value: '/app/mounts' + } + ] + args: [ '--http' ] + volumeMounts: [ + { + volumeName: 'ppt-files-volume' + mountPath: '/app/mounts' + } + ] + } + ] + managedIdentities: { + systemAssigned: false + userAssignedResourceIds: [ + mcpPptFontFixIdentity.outputs.resourceId + ] + } + registries: [ + { + server: containerRegistry.outputs.loginServer + identity: mcpPptFontFixIdentity.outputs.resourceId + } + ] + corsPolicy: { + allowedOrigins: [ + 'https://make.preview.powerautomate.com' + 'https://make.preview.powerapps.com' + 'https://copilotstudio.preview.microsoft.com' + 'https://make.powerautomate.com' + 'https://make.powerapps.com' + 'https://copilotstudio.microsoft.com' + ] + } + environmentResourceId: containerAppsEnvironment.outputs.resourceId + location: location + tags: union(tags, { 'azd-service-name': 'ppt-font-fix' }) + } +} +resource authConfig 'Microsoft.App/containerApps/authConfigs@2023-05-01' = { + name: '${containerAppName}/current' + properties: { + platform: { + enabled: false + } + globalValidation: { + unauthenticatedClientAction: 'AllowAnonymous' + } + } + dependsOn: [ + mcpPptFontFix + ] +} + +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer +output AZURE_RESOURCE_MCP_PPT_FONT_FIX_ID string = mcpPptFontFix.outputs.resourceId +output AZURE_RESOURCE_MCP_PPT_FONT_FIX_NAME string = mcpPptFontFix.outputs.name +output AZURE_RESOURCE_MCP_PPT_FONT_FIX_FQDN string = mcpPptFontFix.outputs.fqdn +output AZURE_STORAGE_ACCOUNT_NAME string = storage.name +output AZURE_FILE_SHARE_NAME string = storageFileShare.name +output AZURE_BLOB_CONTAINER_NAME string = generatedFilesContainer.name diff --git a/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Configurations/PPTFontFixAppSettings.cs b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Configurations/PPTFontFixAppSettings.cs new file mode 100644 index 0000000..56c75ee --- /dev/null +++ b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Configurations/PPTFontFixAppSettings.cs @@ -0,0 +1,64 @@ +using McpSamples.Shared.Configurations; + +using Microsoft.OpenApi.Models; + +namespace McpSamples.PptFontFix.HybridApp.Configurations; + +/// +/// This represents the application settings for ppt-font-fix app. +/// +public class PptFontFixAppSettings : AppSettings +{ + /// + public override OpenApiInfo OpenApi { get; set; } = new() + { + Title = "MCP PPT Font Fix", + Version = "1.0.0", + Description = "A simple MCP server for managing PPT font fixing." + }; + + /// + /// The root path for the workspace. + /// + public string WorkspacePath { get; set; } = string.Empty; + + /// + /// The path for generated files. + /// + public string GeneratedPath { get; set; } = string.Empty; + + /// + /// The path for input files. + /// + public string InputPath { get; set; } = string.Empty; + + /// + /// Indicates whether the application is running in HTTP mode. + /// + public bool IsHttpMode { get; set; } + /// + /// Indicates whether the application is running in a containerized environment. + /// + public bool IsContainer { get; set; } + /// + /// Indicates whether the application is running in an Azure environment. + /// + public bool IsAzure { get; set; } + + /// + protected override T ParseMore(IConfiguration config, string[] args) + { + var settings = base.ParseMore(config, args); + + for (var i = 0; i < args.Length; i++) + { + var arg = args[i]; + if (args.Contains("-c") || args.Contains("--container")) + { + (settings as PptFontFixAppSettings)!.IsContainer = true; + } + } + + return (settings as T)!; + } +} \ No newline at end of file diff --git a/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/McpSamples.PptFontFix.HybridApp.csproj b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/McpSamples.PptFontFix.HybridApp.csproj new file mode 100644 index 0000000..87c917a --- /dev/null +++ b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/McpSamples.PptFontFix.HybridApp.csproj @@ -0,0 +1,22 @@ + + + + Exe + net9.0 + enable + enable + bfd28526-fe6f-46bc-8552-ea87be54575f + + + + + + + + + + + + + + diff --git a/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Models/PPTFontAnalyzeResult.cs b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Models/PPTFontAnalyzeResult.cs new file mode 100644 index 0000000..a930994 --- /dev/null +++ b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Models/PPTFontAnalyzeResult.cs @@ -0,0 +1,49 @@ +namespace McpSamples.PptFontFix.HybridApp.Models; + +/// +/// This represents the result of Ppt font analysis. +/// + +public class PptFontAnalyzeResult +{ + /// + /// Gets or sets the list of used fonts in the presentation. + /// + public List UsedFonts { get; set; } = new List(); + + /// + /// Gets or sets the list of unused fonts in the presentation. + /// + public List UnusedFonts { get; set; } = new List(); + + /// + /// Gets or sets the list of inconsistently used fonts in the presentation. + /// + public List InconsistentlyUsedFonts { get; set; } = new List(); + + /// + /// Gets or sets the list of inconsistently used font locations in the presentation. + /// + public List InconsistentFontLocations { get; set; } = new List(); + + /// + /// Gets or sets the list of unused font locations in the presentation. + /// + public List UnusedFontLocations { get; set; } = new List(); +} + +/// +/// This represents the location where a font is used in the presentation. +/// +public class FontUsageLocation +{ + /// + /// Gets or sets the slide number where the font is used. + /// + public int SlideNumber { get; set; } + + /// + /// Gets or sets the shape name where the font is used. + /// + public string ShapeName { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Program.cs b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Program.cs new file mode 100644 index 0000000..280db37 --- /dev/null +++ b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Program.cs @@ -0,0 +1,133 @@ +using McpSamples.PptFontFix.HybridApp.Configurations; +using McpSamples.PptFontFix.HybridApp.Services; +using McpSamples.PptFontFix.HybridApp.Tools; +using McpSamples.PptFontFix.HybridApp.Prompts; +using McpSamples.Shared.Configurations; +using McpSamples.Shared.Extensions; +using McpSamples.Shared.OpenApi; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; + +var useStreamableHttp = AppSettings.UseStreamableHttp(Environment.GetEnvironmentVariables(), args); + +IHostApplicationBuilder builder = useStreamableHttp + ? WebApplication.CreateBuilder(args) + : Host.CreateApplicationBuilder(args); + +builder.Services.AddAppSettings(builder.Configuration, args); + +builder.Services.AddHttpContextAccessor(); +builder.Services.AddSingleton(); + +builder.Services.AddTransient(); +builder.Services.AddSingleton(); + +IHost app = builder.BuildApp(useStreamableHttp); + +var appSettings = app.Services.GetRequiredService(); +InitializeRuntimeSettings(appSettings, useStreamableHttp); + +if (useStreamableHttp && app is WebApplication webApp) +{ + string actualGeneratedPath = appSettings.GeneratedPath; + if (!Directory.Exists(actualGeneratedPath)) + { + Directory.CreateDirectory(actualGeneratedPath); + } + + webApp.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(actualGeneratedPath), + RequestPath = "/generated", + ServeUnknownFileTypes = true + }); + webApp.MapPost("/upload", async (IFormFile file, IPptFontFixService service) => + { + if (file is null || file.Length == 0) + { + return Results.BadRequest(new { error = "No file uploaded." }); + } + + var appSettings = webApp.Services.GetRequiredService(); + if (!Directory.Exists(appSettings.InputPath)) + Directory.CreateDirectory(appSettings.InputPath); + + var fileName = Path.GetFileName(file.FileName); + var filePath = Path.Combine(appSettings.InputPath, fileName); + + try + { + using (var stream = file.OpenReadStream()) + using (var outputStream = File.Create(filePath)) + { + await stream.CopyToAsync(outputStream); + } + + return Results.Ok(new { message = "File uploaded successfully.", filePath = filePath }); + } + catch (Exception ex) + { + return Results.Problem($"File upload failed: {ex.Message}"); + } + }) + .DisableAntiforgery(); +} + +await app.RunAsync(); + +void InitializeRuntimeSettings(PptFontFixAppSettings settings, bool isHttp) +{ + bool isContainer = settings.IsContainer || Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true"; + string? azureAppName = Environment.GetEnvironmentVariable("CONTAINER_APP_NAME"); + bool isAzure = !string.IsNullOrEmpty(azureAppName); + + string baseDirectory; + + if (isAzure) + { + baseDirectory = "/app"; + settings.InputPath = Path.Combine(baseDirectory, "workspace/input"); + settings.GeneratedPath = Path.Combine(baseDirectory,"workspace/generated"); + settings.WorkspacePath = Path.GetDirectoryName(settings.InputPath) ?? baseDirectory; + } + else if (isContainer) + { + baseDirectory = "/files"; + settings.InputPath = Path.Combine(baseDirectory, "input"); + settings.GeneratedPath = Path.Combine(baseDirectory, "generated"); + + settings.WorkspacePath = Path.GetDirectoryName(settings.InputPath) ?? baseDirectory; + } + else + { + baseDirectory = TryFindProjectRoot(Directory.GetCurrentDirectory()) ?? Directory.GetCurrentDirectory(); + string workspacePath = Path.Combine(baseDirectory, "workspace"); + settings.WorkspacePath = workspacePath; + settings.InputPath = Path.Combine(workspacePath, "input"); + settings.GeneratedPath = Path.Combine(workspacePath, "generated"); + } + + settings.IsHttpMode = isHttp; + settings.IsContainer = isContainer; + settings.IsAzure = isAzure; + + if (!Directory.Exists(settings.WorkspacePath)) Directory.CreateDirectory(settings.WorkspacePath); + if (!Directory.Exists(settings.InputPath)) Directory.CreateDirectory(settings.InputPath); + if (!Directory.Exists(settings.GeneratedPath)) Directory.CreateDirectory(settings.GeneratedPath); +} + +string? TryFindProjectRoot(string startPath) +{ + var dir = new DirectoryInfo(startPath); + while (dir != null) + { + if (dir.GetFiles("Dockerfile.ppt-font-fix").Length > 0) + { + return Path.Combine(dir.FullName, "ppt-font-fix"); + } + dir = dir.Parent; + } + return null; +} diff --git a/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Prompts/PptFontFixPrompt.cs b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Prompts/PptFontFixPrompt.cs new file mode 100644 index 0000000..ffc36fe --- /dev/null +++ b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Prompts/PptFontFixPrompt.cs @@ -0,0 +1,61 @@ +using System.ComponentModel; +using ModelContextProtocol.Server; +using System.IO; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace McpSamples.PptFontFix.HybridApp.Prompts; + +/// +/// This provides interfaces for PPT Font Fix prompts. +/// +public interface IPptFontFixPrompt +{ + /// + /// Gets a prompt to start the PPT font fix workflow. + /// + /// The full path to the PPTX file on the host machine or a public URL. + string GetAnalysisPrompt(string hostFilePath); +} + +/// +/// This represents the prompts entity for the PptFontFix system. +/// +[McpServerPromptType] +public class PptFontFixPrompt : IPptFontFixPrompt +{ + + /// + [McpServerPrompt(Name = "fix_ppt_fonts", Title = "Start PPT Font Fix Workflow")] + [Description("Generate a structured workflow prompt for analyzing and fixing PPT fonts.")] + public string GetAnalysisPrompt( + [Description("The full path to the PPTX file or a public URL.")] string hostFilePath) + { + return $""" + You are a professional PPT Font Fix helper. + Your task is to open a PPT File, perform font analysis, modification, and storage based on this input (File Path : '{hostFilePath}'. + + ### Order of execution + 1. Use the 'open_ppt_file' tool to verify that the file exists in FilePath. If the file opens without any problems, proceed to the next step. + - If not, use the file system tool to move the file in the File Path that you suggested to a specific path based on the result value returned by the tool. + - If Tool returns 'curl' command, use the file system tool to download the file from the public URL to the specific path. + 2. If the file is opened normally, please analyze the usage status of the font in the PPT file through 'analyze_font' and show the following results. + - List of used fonts + - List of inconsistent used fonts + 3. If an item exists in the unused font: + - Ask the user for two answers. + A. **Choose a Standard Font** (from UsedFonts) + B. **Choose an Action Mode:**: + 1. Fix & Clean — Replace fonts and remove unused text boxes + 2. Fix Only — Replace fonts only + ** Make sure to enter the font name as it is in the original, not Unicode. ** + 4. Ask the user if there is a path they want to save. + - If the user did not answer the desired path: Save it as the default path if the file was opened immediately without any problems in No. 1, and specify the outputDirectory as null if you moved the file in the path you suggested. + ** If the returned path starts with '/files/', the file is stored within the mounted path. If the user has responded to the desired path, this requires a file system tool to copy it from the mounted folder to the file path between the local and the container. If not, it should be notified that it is stored in the mounted folder. (Exists inside workspace) + ** If result is link, return the link directly to the user. ** + 5. Report + Provides the download link or file path that the Tool returns. + """; + } +} \ No newline at end of file diff --git a/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Properties/launchSettings.json b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Properties/launchSettings.json new file mode 100644 index 0000000..f518aeb --- /dev/null +++ b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5166", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7013;http://localhost:5166", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Services/PPTFontFixService.cs b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Services/PPTFontFixService.cs new file mode 100644 index 0000000..0747110 --- /dev/null +++ b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Services/PPTFontFixService.cs @@ -0,0 +1,454 @@ +using ShapeCrawler; +using Microsoft.Extensions.Logging; +using ModelContextProtocol.Server; +using System.Linq; +using McpSamples.PptFontFix.HybridApp.Models; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Http; +using System.IO; +using System.Threading; +using Azure.Storage.Sas; +using McpSamples.PptFontFix.HybridApp.Configurations; + +namespace McpSamples.PptFontFix.HybridApp.Services; + +/// +/// This provides interface for Ppt font fixing operations. +/// +public interface IPptFontFixService +{ + + /// + /// open a Ppt file. + /// + /// + Task OpenPptFileAsync(string filePath); + + /// + /// Analyze fonts in a Ppt file. + /// + /// A classified list of fonts used in the presentation. + Task AnalyzeFontsAsync(); + + /// + /// Save the modified Ppt file. + /// + /// The desired file name to save as. + /// The directory path on the host machine to save the modified file. + Task SavePptFileAsync(string desiredFileName, string? outputDirectory = null); + + /// + /// Remove Unused Fonts from the presentation. + /// + /// A list of shape locations to be removed. + Task RemoveUnusedFontsAsync(List locationsToRemove); + + /// + /// Replace a font with another font throughout the presentation. + /// + /// The font name to be replaced. + /// The replacement font name. + /// The number of replacements made. + Task ReplaceFontAsync(string fontToReplace, string replacementFont); +} + +/// +/// This represents the service entity for Ppt font fixing. +/// +/// +public class PptFontFixService : IPptFontFixService +{ + private readonly ILogger _logger; + private readonly IHttpContextAccessor? _httpContextAccessor; + private readonly PptFontFixAppSettings _settings; + private Presentation? _presentation; + + private HashSet? _analyzedVisibleFonts; + + public PptFontFixService( + ILogger logger, + IConfiguration configuration, + IHostEnvironment hostEnvironment, + PptFontFixAppSettings settings, + IServiceProvider serviceProvider) + { + _logger = logger; + _httpContextAccessor = serviceProvider.GetService(); + _settings = settings; + + } + + + /// + public async Task OpenPptFileAsync(string filePath) + { + ArgumentException.ThrowIfNullOrWhiteSpace(filePath, nameof(filePath)); + + _presentation?.Dispose(); + _presentation = null; + _analyzedVisibleFonts = null; + + if (Uri.TryCreate(filePath, UriKind.Absolute, out var uriResult) + && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps)) + { + _logger.LogError("File path cannot be a URL. Must be a local or mounted path."); + return "[Error] File Access: Direct URL access is not supported. Please ensure the file is placed in the shared volume."; + } + + string safeFileName = Path.GetFileName(filePath.Replace('\\', '/')); + + var searchPaths = new List { filePath, Path.Combine(_settings.InputPath, safeFileName) }; + + string? foundPath = null; + foreach (var path in searchPaths) + { + if (File.Exists(path)) + { + foundPath = path; + _logger.LogInformation("✅ File found at: {Path}", foundPath); + break; + } + } + + if (foundPath == null) + { + if (_settings.IsAzure && _settings.IsHttpMode) + { + string uploadUrl = "/upload"; + var request = _httpContextAccessor?.HttpContext?.Request; + if (request != null) + { + uploadUrl = $"{request.Scheme}://{request.Host}/upload"; + } + + return $""" + ⚠️ Action Required: File Upload Needed (Azure Remote) + + I am running in an Azure environment and cannot access your local file `{filePath}` directly. + To proceed, you need to upload the file to the server. + + Please execute the appropriate command for your operating system (OS) using your terminal or shell tool: + + ### 🍎/🐧 macOS & Linux (Example) + + curl -F "file=@{filePath}" {uploadUrl} + + ### 💻 Windows (PowerShell/CMD Example) + + curl.exe -F "file=@{filePath}" {uploadUrl} + + After the command completes successfully, please call the `open_ppt_file` tool again with the path returned by the upload. + """; + } + else if (_settings.IsContainer) + { + return $""" + ⚠️ Action Required: File Synchronization Needed (Docker Container) + + The file '{safeFileName}' (Original path: `{filePath}`) is not currently in the shared volume visible to the container. + To proceed, I need access to this file. + + EXECUTION PLAN (Follow Strictly): + 1. COPY the file from your local machine path (`{filePath}`) + to the host directory mapped to the container volume. + The recommended target path is: `{_settings.InputPath}/{safeFileName}` + 2. RETRY the `open_ppt_file` tool immediately with the original file path (`{filePath}`). + """; + } + + _logger.LogError("❌ File not found. Searched in: {Paths}", string.Join(", ", searchPaths)); + return $"[Error] File Not Found: The file '{filePath}' was not found. Please ensure the path is correct and accessible."; + } + + try + { + _presentation?.Dispose(); + _presentation = new ShapeCrawler.Presentation(foundPath); + _analyzedVisibleFonts = null; + _logger.LogInformation("Ppt file opened successfully: {FilePath}", foundPath); + return "✅ Ppt file opened successfully and ready for analysis."; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to open Ppt file from path: {Path}", foundPath); + return $"[Error] Failed to open Ppt file: {ex.Message}"; + } + } + + /// + public async Task AnalyzeFontsAsync() + { + if (this._presentation == null) + { + throw new InvalidOperationException("Ppt file is not opened. Please open a Ppt file before analyzing fonts."); + } + + var totalFontsInSlides = new HashSet(); + var visibleFontUsages = new Dictionary>(); + var result = new PptFontAnalyzeResult(); + var slideWidth = this._presentation.SlideWidth; + var slideHeight = this._presentation.SlideHeight; + + Action processPortion = (portion, isShapeVisible, isSlideVisible, location) => + { + if (portion.GetType().Name == "ParagraphLineBreak") + { + return; + } + if (portion.Font == null) return; + string? fontName = null; + + try + { + fontName = portion.Font.LatinName; + } + catch (NullReferenceException ex) + { + _logger.LogWarning(ex, + "NullReferenceException occurred while getting LatinName for Shape: {ShapeName} in Slide: {SlideNumber}. Skipping portion.", + location.ShapeName, location.SlideNumber); + return; + } + + if (string.IsNullOrEmpty(fontName)) + { + return; + } + + totalFontsInSlides.Add(fontName); + + if (isSlideVisible && isShapeVisible) + { + bool isWhitespace = string.IsNullOrWhiteSpace(portion.Text); + if (!isWhitespace) + { + if (!visibleFontUsages.TryGetValue(fontName, out List? value)) + { + value = []; + visibleFontUsages[fontName] = value; + } + + value.Add(location); + } + } + }; + + foreach (var slide in this._presentation.Slides) + { + bool isSlideVisible = !slide.Hidden(); + foreach (var shape in slide.Shapes) + { + var location = new FontUsageLocation { SlideNumber = slide.Number, ShapeName = shape.Name }; + + bool isEmptyBox = shape.TextBox != null && string.IsNullOrWhiteSpace(shape.TextBox.Text); + if (isEmptyBox) + { + _logger.LogWarning("[Empty Box Detected] Slide Number: {SlideNumber}, Shape Name: {ShapeName}", + slide.Number, shape.Name); + result.UnusedFontLocations.Add(location); + } + + bool isShapeVisible = !((shape.X + shape.Width) <= 0 || + shape.X >= slideWidth || + (shape.Y + shape.Height) <= 0 || + shape.Y >= slideHeight); + + if (!isShapeVisible && shape.TextBox != null && !isEmptyBox) + { + _logger.LogWarning("[Text Shape Outside Slide] Slide Number: {SlideNumber}, Shape Name: {ShapeName}", + slide.Number, shape.Name); + result.UnusedFontLocations.Add(location); + } + + if (shape.TextBox != null) + { + foreach (var portion in shape.TextBox.Paragraphs.SelectMany(p => p.Portions)) + { + processPortion(portion, isShapeVisible, isSlideVisible, location); + } + } + } + } + + var allVisibleFontNames = new HashSet(visibleFontUsages.Keys); + + this._analyzedVisibleFonts = new HashSet(allVisibleFontNames, StringComparer.OrdinalIgnoreCase); + + var unusedFonts = new HashSet(totalFontsInSlides); + unusedFonts.ExceptWith(allVisibleFontNames); + result.UnusedFonts = unusedFonts.ToList(); + int standardFontCount = 2; + var sortedVisibleFonts = visibleFontUsages + .OrderByDescending(pair => pair.Value.Count) + .ToList(); + result.UsedFonts = sortedVisibleFonts + .Take(standardFontCount) + .Select(pair => pair.Key) + .ToList(); + var inconsistentPairs = sortedVisibleFonts + .Skip(standardFontCount) + .ToList(); + result.InconsistentlyUsedFonts = inconsistentPairs + .Select(pair => pair.Key) + .ToList(); + result.InconsistentFontLocations = inconsistentPairs + .SelectMany(pair => pair.Value) + .ToList(); + + _logger.LogInformation("[Result] Used (Standard) Fonts: {Fonts}", string.Join(", ", result.UsedFonts)); + _logger.LogInformation("[Result] Unused Fonts: {Fonts}", string.Join(", ", result.UnusedFonts)); + _logger.LogInformation("[Result] Inconsistently Used Fonts: {Fonts}", string.Join(", ", result.InconsistentlyUsedFonts)); + + return await Task.FromResult(result); + } + + /// + public async Task SavePptFileAsync(string desiredFileName, string? outputDirectory = null) + { + if (this._presentation == null) throw new InvalidOperationException("Ppt file is not opened. Please open a Ppt file before saving."); + ArgumentException.ThrowIfNullOrWhiteSpace(desiredFileName, nameof(desiredFileName)); + + string safeFileName = Path.GetFileName(desiredFileName).Replace(":", "").Trim(); + + _logger.LogInformation("Save process started. Target: {SafeName}", safeFileName); + + using (var memoryStream = new MemoryStream()) + { + await Task.Run(() => _presentation.Save(memoryStream)); + memoryStream.Position = 0; + + string baseDirectory = _settings.GeneratedPath; + + if (!string.IsNullOrEmpty(outputDirectory) && !_settings.IsContainer && !_settings.IsAzure) + { + baseDirectory = outputDirectory; + _logger.LogInformation("Using User Provided Directory: {Path}", baseDirectory); + } + + if (!Directory.Exists(baseDirectory)) Directory.CreateDirectory(baseDirectory); + + string finalPhysicalPath = Path.Combine(baseDirectory, safeFileName); + + try + { + using (var fs = new FileStream(finalPhysicalPath, FileMode.Create, FileAccess.Write)) + { + await memoryStream.CopyToAsync(fs); + await fs.FlushAsync(); + } + + _logger.LogInformation("File saved to: {Path}", finalPhysicalPath); + } + catch (Exception ex) + { + _logger.LogError(ex, "FATAL: Could not save file."); + throw; + } + + if (_settings.IsHttpMode) + { + var request = _httpContextAccessor?.HttpContext?.Request; + if (request != null) + { + string url = $"{request.Scheme}://{request.Host}/generated/{safeFileName}"; + _logger.LogInformation("Returning Web URL: {Url}", url); + return url; + } + } + + return finalPhysicalPath; + } + } + + /// + public async Task RemoveUnusedFontsAsync(List locationsToRemove) + { + if (this._presentation == null) + { + throw new InvalidOperationException("Ppt file is not opened. Please open a Ppt file before removing unused fonts."); + } + + if (locationsToRemove == null || locationsToRemove.Count == 0) + { + _logger.LogInformation("No locations provided for removal. Skipping removal process."); + return await Task.FromResult(0); + } + + int removalCount = 0; + + foreach (var location in locationsToRemove) + { + var slide = this._presentation.Slides.FirstOrDefault(s => s.Number == location.SlideNumber); + if (slide == null) + { + _logger.LogWarning("Slide number {SlideNumber} not found. Skipping.", location.SlideNumber); + continue; + } + + var shape = slide.Shapes.FirstOrDefault(sh => sh.Name == location.ShapeName); + if (shape == null) + { + _logger.LogWarning("Shape name {ShapeName} not found in slide {SlideNumber}. Skipping.", location.ShapeName, location.SlideNumber); + continue; + } + + shape.Remove(); + removalCount++; + _logger.LogInformation("Removed shape {ShapeName} from slide {SlideNumber}.", location.ShapeName, location.SlideNumber); + } + + _logger.LogInformation("Total shapes removed: {Count}", removalCount); + return await Task.FromResult(removalCount); + } + + /// + public async Task ReplaceFontAsync(string fontToReplace, string replacementFont) + { + if (this._presentation == null) + { + throw new InvalidOperationException("Ppt file is not opened. Please open a Ppt file before replacing fonts."); + } + + if (string.IsNullOrWhiteSpace(fontToReplace)) + { + throw new ArgumentException("Font to replace cannot be null or whitespace.", nameof(fontToReplace)); + } + + if (string.IsNullOrWhiteSpace(replacementFont)) + { + throw new ArgumentException("Replacement font cannot be null or whitespace.", nameof(replacementFont)); + } + + if (!this._analyzedVisibleFonts.Contains(replacementFont)) + { + throw new ArgumentException($"The font '{replacementFont}' is not a valid font found in the presentation's visible text. Please choose from the analyzed list.", nameof(replacementFont)); + } + + + int replacementCount = 0; + + foreach (var slide in this._presentation.Slides) + { + foreach (var shape in slide.Shapes) + { + if (shape.TextBox != null) + { + foreach (var portion in shape.TextBox.Paragraphs.SelectMany(p => p.Portions)) + { + if (portion.Font != null && string.Equals(portion.Font.LatinName, fontToReplace, StringComparison.OrdinalIgnoreCase)) + { + portion.Font.LatinName = replacementFont; + replacementCount++; + } + } + } + } + } + + _logger.LogInformation("Total font replacements made: {Count}", replacementCount); + return await Task.FromResult(replacementCount); + } +} \ No newline at end of file diff --git a/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Tools/PPTFontFixTool.cs b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Tools/PPTFontFixTool.cs new file mode 100644 index 0000000..5c9145a --- /dev/null +++ b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/Tools/PPTFontFixTool.cs @@ -0,0 +1,138 @@ +using Microsoft.Extensions.Logging; +using System.ComponentModel; +using System; +using System.Threading.Tasks; +using McpSamples.PptFontFix.HybridApp.Services; +using McpSamples.PptFontFix.HybridApp.Models; + +using ModelContextProtocol.Server; + +namespace McpSamples.PptFontFix.HybridApp.Tools; + +/// +/// This provides interface for the Ppt font fix tool. +/// + +public interface IPptFontFixTool +{ + /// + /// Opens the specified PPT file and loads it into memory for subsequent analysis. + /// + /// The path to the PPT file. + /// A string message indicating success or an action required instruction for the LLM. + Task OpenPptFileAsync(string filePath); + + /// + /// Analyzes fonts in the file currently loaded in memory. + /// + /// Returns instance. + Task AnalyzeFontsAsync(); + + /// + /// Updates the PPT file by removing unused fonts, replacing inconsistent fonts, and saving to a new path. + /// + /// The font to replace all inconsistent fonts with. + /// The list of inconsistent font names to be replaced. + /// The list of shape locations to be removed. + /// The directory path on the host machine to save the modified file. + /// The full path to save the new .pptx file. + /// Returns a success message with the new file path. + Task UpdatePptFileAsync(string replacementFont, List inconsistentFontsToReplace, List locationsToRemove, string outputDirectory, string newFileName); +} + +/// +/// This represents the tool entity for Ppt font fixing. +/// +/// instance. +/// instance. +[McpServerToolType] +public class PptFontFixTool(IPptFontFixService service, ILogger logger) : IPptFontFixTool +{ + [McpServerTool(Name = "open_ppt_file", Title = "Open PPT File")] + [Description("Opens a Ppt file and loads it into memory. Returns action instructions if the file cannot be accessed.")] + public async Task OpenPptFileAsync( + [Description("The path of the Ppt file to open and analyze")] string filePath) + { + string? actionRequiredMessage = await service.OpenPptFileAsync(filePath).ConfigureAwait(false); + + if (!string.IsNullOrEmpty(actionRequiredMessage)) + { + return actionRequiredMessage; + } + + logger.LogInformation("Ppt file opened successfully: {FilePath}", filePath); + return $"PPT file '{filePath}' successfully loaded into memory. You can now call the analyze_fonts tool."; + } + + + /// + [McpServerTool(Name = "analyze_fonts", Title = "Analyze Fonts")] + [Description("Analyzes fonts used in the PPT file currently loaded in memory, identifying inconsistencies.")] + public async Task AnalyzeFontsAsync() + { + try + { + PptFontAnalyzeResult result = await service.AnalyzeFontsAsync().ConfigureAwait(false); + if (result != null) + { + int count = result.InconsistentlyUsedFonts?.Count ?? 0; + logger.LogInformation("Font analysis completed. Inconsistently used fonts count: {Count}", count); + return result; + } + else + { + logger.LogWarning("Font analysis service returned a null result."); + return new PptFontAnalyzeResult(); + } + } + catch (InvalidOperationException ex) + { + logger.LogError(ex, "Analysis failed because the PPT file was not loaded."); + throw; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed during font analysis process."); + throw; + } + } + + /// + [McpServerTool(Name = "update_ppt_file", Title = "Update and Save PPT File")] + [Description("Removes unused fonts, replaces inconsistently used fonts with another font defined within the Ppt file, and saves the file to a user-specified path.")] + public async Task UpdatePptFileAsync( + [Description("The replacement font")] string replacementFont, + [Description("The fonts to be replaced")] List inconsistentFontsToReplace, + [Description("A list of shape locations (from analysis result) to be removed")] List locationsToRemove, + [Description("The directory path on the host machine to save the modified file (e.g., C:\\Users\\Downloads)")] string outputDirectory, + [Description("The filename for the modified Ppt file.")] string newFileName) + { + try + { + int removalCount = await service.RemoveUnusedFontsAsync(locationsToRemove).ConfigureAwait(false); + logger.LogInformation("{Count} unused font shapes removed.", removalCount); + + int totalReplacementCount = 0; + if (inconsistentFontsToReplace != null) + { + foreach (var fontToReplace in inconsistentFontsToReplace) + { + int count = await service.ReplaceFontAsync(fontToReplace, replacementFont).ConfigureAwait(false); + totalReplacementCount += count; + } + } + logger.LogInformation("{Count} instances of inconsistent fonts replaced with '{ReplacementFont}'.", totalReplacementCount, replacementFont); + + string accessPath = await service.SavePptFileAsync(newFileName, outputDirectory).ConfigureAwait(false); + + logger.LogInformation("Ppt file saved successfully: {Path}", accessPath); + + return $"PPT update complete. Removed: {removalCount}, Replaced: {totalReplacementCount}. Result: {accessPath}"; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed during Ppt file update process. Save name: {FileName}", newFileName); + throw; + } + } +} \ No newline at end of file diff --git a/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/appsettings.Development.json b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/appsettings.json b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/ppt-font-fix/src/McpSamples.PptFontFix.HybridApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}