From cafe97c86560ccc3473512370c088563f3169b51 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Thu, 9 Oct 2025 17:24:17 +0900 Subject: [PATCH 01/61] Add initial README.md for MCP Server: OpenAPI to SDK --- openapi-to-sdk/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 openapi-to-sdk/README.md diff --git a/openapi-to-sdk/README.md b/openapi-to-sdk/README.md new file mode 100644 index 00000000..8f179794 --- /dev/null +++ b/openapi-to-sdk/README.md @@ -0,0 +1,3 @@ +# MCP Server: Awesome Copilot + +This is an MCP server that integrates with [Kiota](https://github.com/microsoft/kiota) to generate an SDK from OpenAPI doc. \ No newline at end of file From ab83919ca1cd880245b734d2ef6109557f4a2fea Mon Sep 17 00:00:00 2001 From: x-or-b Date: Thu, 9 Oct 2025 17:50:23 +0900 Subject: [PATCH 02/61] Add OpenAPItoSDKAppSettings class and update README.md for clarity --- openapi-to-sdk/README.md | 2 +- .../Configurations/OpenAPItoSDKAppSettings.cs | 18 ++++++++++++++++++ .../Program.cs | 0 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Configurations/OpenAPItoSDKAppSettings.cs create mode 100644 openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs diff --git a/openapi-to-sdk/README.md b/openapi-to-sdk/README.md index 8f179794..c973295f 100644 --- a/openapi-to-sdk/README.md +++ b/openapi-to-sdk/README.md @@ -1,3 +1,3 @@ # MCP Server: Awesome Copilot -This is an MCP server that integrates with [Kiota](https://github.com/microsoft/kiota) to generate an SDK from OpenAPI doc. \ No newline at end of file +This is an MCP server that integrates with [Kiota](https://github.com/microsoft/kiota) to generate an SDK from OpenAPI documents. \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Configurations/OpenAPItoSDKAppSettings.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Configurations/OpenAPItoSDKAppSettings.cs new file mode 100644 index 00000000..86a52791 --- /dev/null +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Configurations/OpenAPItoSDKAppSettings.cs @@ -0,0 +1,18 @@ +using McpSamples.Shared.Configurations; + +using Microsoft.OpenApi.Models; + +namespace McpSamples.OpenAPItoSDK.HybridApp.Configurations; + +/// +/// This represents the application settings for markdown-to-html app. +/// +public class OpenAPItoSDKAppSettings : AppSettings +{ + /// + public override OpenApiInfo OpenApi { get; set; } = new() + { + Title = "MCP OpenAPI to SDK", + Version = "1.0.0", + Description = "A simple MCP server for integrating Kiota to generate an SDK from OpenAPI documents." + }; \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs new file mode 100644 index 00000000..e69de29b From bc44121f00ebbf9439843c08f5cdc365714e2b55 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Thu, 9 Oct 2025 18:26:56 +0900 Subject: [PATCH 03/61] Refactor namespace and class names for consistency in OpenAPI to SDK settings --- .../Configurations/OpenAPItoSDKAppSettings.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Configurations/OpenAPItoSDKAppSettings.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Configurations/OpenAPItoSDKAppSettings.cs index 86a52791..067d2c2a 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Configurations/OpenAPItoSDKAppSettings.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Configurations/OpenAPItoSDKAppSettings.cs @@ -2,12 +2,12 @@ using Microsoft.OpenApi.Models; -namespace McpSamples.OpenAPItoSDK.HybridApp.Configurations; +namespace McpSamples.OpenApiToSdk.HybridApp.Configurations; /// -/// This represents the application settings for markdown-to-html app. +/// This represents the application settings for openapi-to-sdk app. /// -public class OpenAPItoSDKAppSettings : AppSettings +public class OpenApiToSdkAppSettings : AppSettings { /// public override OpenApiInfo OpenApi { get; set; } = new() @@ -15,4 +15,5 @@ public class OpenAPItoSDKAppSettings : AppSettings Title = "MCP OpenAPI to SDK", Version = "1.0.0", Description = "A simple MCP server for integrating Kiota to generate an SDK from OpenAPI documents." - }; \ No newline at end of file + }; +} \ No newline at end of file From 394b07a434761be9fd32a48b0b4708b7b78c2555 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Thu, 9 Oct 2025 19:30:47 +0900 Subject: [PATCH 04/61] Add ISdkGenerationPrompt interface and SdkGenerationPrompt class for SDK generation and validation prompts --- .../Prompts/SdkGenerationPrompt.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs new file mode 100644 index 00000000..fd9c49c0 --- /dev/null +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs @@ -0,0 +1,75 @@ +using ModelContextProtocol.Server; + +namespace McpSamples.OpenApiToSdk.HybridApp.Prompts; + +/// +/// This provides interfaces for SDK generation prompts. +/// +public interface ISdkGenerationPrompt +{ + /// + /// Gets a prompt for generating an SDK from an OpenAPI specification, including parsing Kiota options. + /// + /// The location of the OpenAPI description. + /// The target language for the SDK. + /// Additional user-provided options for Kiota. + /// A formatted prompt for SDK generation with parsing instructions. + string GetSdkGenerationPrompt(string openApi, string language, string? additionalOptions = null); + + /// + /// Gets a prompt for validating an OpenAPI specification. + /// + /// The URL of the OpenAPI specification. + /// A formatted prompt for validating the OpenAPI spec. + string GetValidationPrompt(string openApi); +} + +/// +/// This provides prompts for SDK generation from OpenAPI specs. +/// +[McpServerPromptType] +public class SdkGenerationPrompt : ISdkGenerationPrompt +{ + /// + [McpServerPrompt(Name = "generate_sdk_with_parsing", Title = "Generate SDK from OpenAPI Spec with Kiota Parsing")] + [Description("Provides a structured prompt for parsing Kiota options and generating an SDK.")] + public string GetSdkGenerationPrompt( + [Description("The Location of the OpenAPI description.")] string openApi, + [Description("The target language for the SDK.")] string language, + [Description("Additional options for Kiota (e.g., '--namespace-name MyNamespace').")] string? additionalOptions = null) + { + return $""" + Generate an SDK from the provided OpenAPI specification using Kiota. + + OpenAPI Location: {openApi} + Language: {language} + Additional Options: {additionalOptions ?? "None"} + + Instructions: + - Parse the additional options to valid Kiota command-line arguments. + - Use the 'generate_sdk' tool with the parsed options to process the spec. + - Validate the OpenAPI spec before generation. + - Return the ZIP file URI upon success. + - Handle errors gracefully and provide feedback. + """; + } + + /// + [McpServerPrompt(Name = "validate_openapi_spec", Title = "Validate OpenAPI Specification")] + [Description("Provides a structured prompt for validating an OpenAPI specification before SDK generation.")] + public string GetValidationPrompt( + [Description("The OpenAPI specification URL.")] string openApi) + { + return $""" + Validate the provided OpenAPI specification. + + OpenAPI Location: {openApi} + + Instructions: + - Download and parse the OpenAPI spec. + - Check for syntax errors, missing required fields, and schema validity. + - Report any validation issues or confirm validity. + - Use this validation before proceeding to SDK generation. + """; + } +} \ No newline at end of file From ce38dc486b5eaa87432e0f09ab2324217e7ad750 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Thu, 9 Oct 2025 23:13:44 +0900 Subject: [PATCH 05/61] =?UTF-8?q?openApi=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=EB=A5=BC=20openApiDocUrl=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Prompts/SdkGenerationPrompt.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs index fd9c49c0..655ff1cb 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs @@ -10,18 +10,18 @@ public interface ISdkGenerationPrompt /// /// Gets a prompt for generating an SDK from an OpenAPI specification, including parsing Kiota options. /// - /// The location of the OpenAPI description. + /// The location of the OpenAPI description. /// The target language for the SDK. /// Additional user-provided options for Kiota. /// A formatted prompt for SDK generation with parsing instructions. - string GetSdkGenerationPrompt(string openApi, string language, string? additionalOptions = null); + string GetSdkGenerationPrompt(string openApiDocUrl, string language, string? additionalOptions = null); /// /// Gets a prompt for validating an OpenAPI specification. /// - /// The URL of the OpenAPI specification. + /// The URL of the OpenAPI specification. /// A formatted prompt for validating the OpenAPI spec. - string GetValidationPrompt(string openApi); + string GetValidationPrompt(string openApiDocUrl); } /// @@ -34,14 +34,14 @@ public class SdkGenerationPrompt : ISdkGenerationPrompt [McpServerPrompt(Name = "generate_sdk_with_parsing", Title = "Generate SDK from OpenAPI Spec with Kiota Parsing")] [Description("Provides a structured prompt for parsing Kiota options and generating an SDK.")] public string GetSdkGenerationPrompt( - [Description("The Location of the OpenAPI description.")] string openApi, + [Description("The Location of the OpenAPI description.")] string openApiDocUrl, [Description("The target language for the SDK.")] string language, [Description("Additional options for Kiota (e.g., '--namespace-name MyNamespace').")] string? additionalOptions = null) { return $""" Generate an SDK from the provided OpenAPI specification using Kiota. - OpenAPI Location: {openApi} + OpenAPI Location: {openApiDocUrl} Language: {language} Additional Options: {additionalOptions ?? "None"} @@ -58,12 +58,12 @@ public string GetSdkGenerationPrompt( [McpServerPrompt(Name = "validate_openapi_spec", Title = "Validate OpenAPI Specification")] [Description("Provides a structured prompt for validating an OpenAPI specification before SDK generation.")] public string GetValidationPrompt( - [Description("The OpenAPI specification URL.")] string openApi) + [Description("The OpenAPI specification URL.")] string openApiDocUrl) { return $""" Validate the provided OpenAPI specification. - OpenAPI Location: {openApi} + OpenAPI Location: {openApiDocUrl} Instructions: - Download and parse the OpenAPI spec. From 5e8ededf4604757fa6ea0df07d3abeefb9ea9d4d Mon Sep 17 00:00:00 2001 From: x-or-b Date: Tue, 21 Oct 2025 23:03:03 +0900 Subject: [PATCH 06/61] Initialize McpSamples.OpenApiToSdk.HybridApp project with essential files and configurations --- openapi-to-sdk/McpOpenApiToSdk.sln | 39 +++++++++++++++++++ .../McpSamples.OpenApiToSdk.HybridApp.csproj | 24 ++++++++++++ .../Program.cs | 6 +++ .../Properties/launchSettings.json | 23 +++++++++++ .../appsettings.Development.json | 8 ++++ .../appsettings.json | 9 +++++ 6 files changed, 109 insertions(+) create mode 100644 openapi-to-sdk/McpOpenApiToSdk.sln create mode 100644 openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/McpSamples.OpenApiToSdk.HybridApp.csproj create mode 100644 openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Properties/launchSettings.json create mode 100644 openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/appsettings.Development.json create mode 100644 openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/appsettings.json diff --git a/openapi-to-sdk/McpOpenApiToSdk.sln b/openapi-to-sdk/McpOpenApiToSdk.sln new file mode 100644 index 00000000..334d393b --- /dev/null +++ b/openapi-to-sdk/McpOpenApiToSdk.sln @@ -0,0 +1,39 @@ + +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.OpenApiToSdk.HybridApp", "src\McpSamples.OpenApiToSdk.HybridApp\McpSamples.OpenApiToSdk.HybridApp.csproj", "{F760C2D9-C4DE-45B5-9A04-221958D959E7}" +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 + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|x64.ActiveCfg = Debug|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|x64.Build.0 = Debug|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|x86.ActiveCfg = Debug|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|x86.Build.0 = Debug|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|Any CPU.Build.0 = Release|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|x64.ActiveCfg = Release|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|x64.Build.0 = Release|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|x86.ActiveCfg = Release|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {F760C2D9-C4DE-45B5-9A04-221958D959E7} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + EndGlobalSection +EndGlobal diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/McpSamples.OpenApiToSdk.HybridApp.csproj b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/McpSamples.OpenApiToSdk.HybridApp.csproj new file mode 100644 index 00000000..cb485ff2 --- /dev/null +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/McpSamples.OpenApiToSdk.HybridApp.csproj @@ -0,0 +1,24 @@ + + + + net9.0 + latest + + enable + enable + + McpSamples.OpenApiToSdk.HybridApp + McpSamples.OpenApiToSdk.HybridApp + + 8b94c6d9-feae-4416-8f70-4b50ea51170b + + + + + + + + + + + diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs index e69de29b..1760df1d 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs @@ -0,0 +1,6 @@ +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +app.MapGet("/", () => "Hello World!"); + +app.Run(); diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Properties/launchSettings.json b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Properties/launchSettings.json new file mode 100644 index 00000000..92a407d6 --- /dev/null +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5202", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7205;http://localhost:5202", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/appsettings.Development.json b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/appsettings.json b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From 6bd1c77e2dd9f631b70ebf013c6518ccf846db7c Mon Sep 17 00:00:00 2001 From: x-or-b Date: Sat, 25 Oct 2025 02:35:05 +0900 Subject: [PATCH 07/61] Update solution file to include project configurations for McpSamples.Shared and McpSamples.OpenApiToSdk.HybridApp --- openapi-to-sdk/McpOpenApiToSdk.sln | 74 ++++++++++++++++++------------ 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/openapi-to-sdk/McpOpenApiToSdk.sln b/openapi-to-sdk/McpOpenApiToSdk.sln index 334d393b..adb0e797 100644 --- a/openapi-to-sdk/McpOpenApiToSdk.sln +++ b/openapi-to-sdk/McpOpenApiToSdk.sln @@ -1,39 +1,53 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 +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.Shared", "..\shared\McpSamples.Shared\McpSamples.Shared.csproj", "{D0715E17-D5D1-4062-A2FF-19B696DF623D}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "McpSamples.OpenApiToSdk.HybridApp", "src\McpSamples.OpenApiToSdk.HybridApp\McpSamples.OpenApiToSdk.HybridApp.csproj", "{F760C2D9-C4DE-45B5-9A04-221958D959E7}" 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 - {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|x64.ActiveCfg = Debug|Any CPU - {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|x64.Build.0 = Debug|Any CPU - {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|x86.ActiveCfg = Debug|Any CPU - {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|x86.Build.0 = Debug|Any CPU - {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|Any CPU.Build.0 = Release|Any CPU - {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|x64.ActiveCfg = Release|Any CPU - {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|x64.Build.0 = Release|Any CPU - {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|x86.ActiveCfg = Release|Any CPU - {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {F760C2D9-C4DE-45B5-9A04-221958D959E7} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} - EndGlobalSection + 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 + {D0715E17-D5D1-4062-A2FF-19B696DF623D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0715E17-D5D1-4062-A2FF-19B696DF623D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0715E17-D5D1-4062-A2FF-19B696DF623D}.Debug|x64.ActiveCfg = Debug|Any CPU + {D0715E17-D5D1-4062-A2FF-19B696DF623D}.Debug|x64.Build.0 = Debug|Any CPU + {D0715E17-D5D1-4062-A2FF-19B696DF623D}.Debug|x86.ActiveCfg = Debug|Any CPU + {D0715E17-D5D1-4062-A2FF-19B696DF623D}.Debug|x86.Build.0 = Debug|Any CPU + {D0715E17-D5D1-4062-A2FF-19B696DF623D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0715E17-D5D1-4062-A2FF-19B696DF623D}.Release|Any CPU.Build.0 = Release|Any CPU + {D0715E17-D5D1-4062-A2FF-19B696DF623D}.Release|x64.ActiveCfg = Release|Any CPU + {D0715E17-D5D1-4062-A2FF-19B696DF623D}.Release|x64.Build.0 = Release|Any CPU + {D0715E17-D5D1-4062-A2FF-19B696DF623D}.Release|x86.ActiveCfg = Release|Any CPU + {D0715E17-D5D1-4062-A2FF-19B696DF623D}.Release|x86.Build.0 = Release|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|x64.ActiveCfg = Debug|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|x64.Build.0 = Debug|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|x86.ActiveCfg = Debug|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Debug|x86.Build.0 = Debug|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|Any CPU.Build.0 = Release|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|x64.ActiveCfg = Release|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|x64.Build.0 = Release|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|x86.ActiveCfg = Release|Any CPU + {F760C2D9-C4DE-45B5-9A04-221958D959E7}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D0715E17-D5D1-4062-A2FF-19B696DF623D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {F760C2D9-C4DE-45B5-9A04-221958D959E7} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + EndGlobalSection EndGlobal From 57129e6d26ba52b0a0114cf0f926a12b7a9446b6 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Sun, 2 Nov 2025 21:21:25 +0900 Subject: [PATCH 08/61] Add configuration files for HTTP and STDIO servers in VSCode --- openapi-to-sdk/.vscode/mcp.http.local.json | 8 ++++++++ openapi-to-sdk/.vscode/mcp.stdio.local.json | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 openapi-to-sdk/.vscode/mcp.http.local.json create mode 100644 openapi-to-sdk/.vscode/mcp.stdio.local.json diff --git a/openapi-to-sdk/.vscode/mcp.http.local.json b/openapi-to-sdk/.vscode/mcp.http.local.json new file mode 100644 index 00000000..5a5e6258 --- /dev/null +++ b/openapi-to-sdk/.vscode/mcp.http.local.json @@ -0,0 +1,8 @@ +{ + "servers": { + "todo-list": { + "type": "http", + "url": "http://localhost:5252/mcp" + } + } +} \ No newline at end of file diff --git a/openapi-to-sdk/.vscode/mcp.stdio.local.json b/openapi-to-sdk/.vscode/mcp.stdio.local.json new file mode 100644 index 00000000..b2f49e9f --- /dev/null +++ b/openapi-to-sdk/.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": { + "openapi-to-sdk": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "${input:consoleapp-project-path}" + ] + } + } +} \ No newline at end of file From 16f924913ff7afa12992441fde77cad5d4ad1a52 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Sun, 2 Nov 2025 21:33:19 +0900 Subject: [PATCH 09/61] Change http port number 5252 to 5288 --- openapi-to-sdk/.vscode/mcp.http.local.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi-to-sdk/.vscode/mcp.http.local.json b/openapi-to-sdk/.vscode/mcp.http.local.json index 5a5e6258..72a88ed3 100644 --- a/openapi-to-sdk/.vscode/mcp.http.local.json +++ b/openapi-to-sdk/.vscode/mcp.http.local.json @@ -2,7 +2,7 @@ "servers": { "todo-list": { "type": "http", - "url": "http://localhost:5252/mcp" + "url": "http://localhost:5288/mcp" } } } \ No newline at end of file From 5355f9e84bfa9c241735915f991ecedf7b8633e0 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Sun, 2 Nov 2025 21:40:14 +0900 Subject: [PATCH 10/61] 5202 --- openapi-to-sdk/.vscode/mcp.http.local.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi-to-sdk/.vscode/mcp.http.local.json b/openapi-to-sdk/.vscode/mcp.http.local.json index 72a88ed3..c56fa520 100644 --- a/openapi-to-sdk/.vscode/mcp.http.local.json +++ b/openapi-to-sdk/.vscode/mcp.http.local.json @@ -2,7 +2,7 @@ "servers": { "todo-list": { "type": "http", - "url": "http://localhost:5288/mcp" + "url": "http://localhost:5202/mcp" } } } \ No newline at end of file From c2d408a5809b2d4e22cf46b988db89b9910a292e Mon Sep 17 00:00:00 2001 From: x-or-b Date: Sun, 2 Nov 2025 21:54:15 +0900 Subject: [PATCH 11/61] local settings --- openapi-to-sdk/.vscode/mcp.http.local.json | 2 +- .../Properties/launchSettings.json | 10 +++++----- .../McpSamples.OpenAPItoSDK.HybridApp/appsettings.json | 5 +++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/openapi-to-sdk/.vscode/mcp.http.local.json b/openapi-to-sdk/.vscode/mcp.http.local.json index c56fa520..da096d0a 100644 --- a/openapi-to-sdk/.vscode/mcp.http.local.json +++ b/openapi-to-sdk/.vscode/mcp.http.local.json @@ -2,7 +2,7 @@ "servers": { "todo-list": { "type": "http", - "url": "http://localhost:5202/mcp" + "url": "http://localhost:5222/mcp" } } } \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Properties/launchSettings.json b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Properties/launchSettings.json index 92a407d6..69b4af34 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Properties/launchSettings.json +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Properties/launchSettings.json @@ -4,8 +4,8 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:5202", + "launchBrowser": false, + "applicationUrl": "http://localhost:5222", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -13,11 +13,11 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "https://localhost:7205;http://localhost:5202", + "launchBrowser": false, + "applicationUrl": "https://localhost:45222;http://localhost:5222", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } -} +} \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/appsettings.json b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/appsettings.json index 10f68b8c..65ff64ad 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/appsettings.json +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/appsettings.json @@ -5,5 +5,6 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" -} + "AllowedHosts": "*", + "UseHttp": false +} \ No newline at end of file From 77cd27698f67fe042e57c3bbbc194452e1d5bc80 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Sun, 2 Nov 2025 22:35:03 +0900 Subject: [PATCH 12/61] Refactor Program.cs and add OpenApiToSdkTool class --- .../Program.cs | 17 +++++++++++++---- .../Tools/OpenApiToSdkTool.cs | 6 ++++++ 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs index 1760df1d..c7434a81 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs @@ -1,6 +1,15 @@ -var builder = WebApplication.CreateBuilder(args); -var app = builder.Build(); +using McpSamples.OpenApiToSdk.HybridApp.Configurations; +using McpSamples.Shared.Configurations; +using McpSamples.Shared.Extensions; -app.MapGet("/", () => "Hello World!"); +var useStreamableHttp = AppSettings.UseStreamableHttp(Environment.GetEnvironmentVariables(), args); -app.Run(); +IHostApplicationBuilder builder = useStreamableHttp + ? WebApplication.CreateBuilder(args) + : Host.CreateApplicationBuilder(args); + +builder.Services.AddAppSettings(builder.Configuration, args); + +IHost app = builder.BuildApp(useStreamableHttp); + +await app.RunAsync(); \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs new file mode 100644 index 00000000..3649ce22 --- /dev/null +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs @@ -0,0 +1,6 @@ +namespace McpSamples.OpenApiToSdk.HybridApp.Tools; + +public class OpenApiToSdkTool +{ + +} \ No newline at end of file From 566a01f641007d793caca64d2dfc0c5f53b00436 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Mon, 3 Nov 2025 01:42:01 +0900 Subject: [PATCH 13/61] Download openapi spec --- .vscode/mcp.json | 34 ++++++------------- .../Program.cs | 8 +++++ .../Services/IOpenApiService.cs | 15 ++++++++ .../Services/OpenApiService.cs | 30 ++++++++++++++++ .../Tools/OpenApiToSdkTool.cs | 33 +++++++++++++++++- 5 files changed, 96 insertions(+), 24 deletions(-) create mode 100644 openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/IOpenApiService.cs create mode 100644 openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 3bd62f4f..61c9bfd9 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,31 +1,19 @@ { + "inputs": [ + { + "type": "promptString", + "id": "consoleapp-project-path", + "description": "The absolute path to the console app project Directory" + } + ], "servers": { - "awesome-copilot": { - "type": "stdio", - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "ghcr.io/microsoft/mcp-dotnet-samples/awesome-copilot:latest" - ] - }, - "microsoft.docs.mcp": { - "type": "http", - "url": "https://learn.microsoft.com/api/mcp" - }, - "github": { - "type": "http", - "url": "https://api.githubcopilot.com/mcp/" - }, - "sequential-thinking": { + "openapi-to-sdk": { "type": "stdio", - "command": "docker", + "command": "dotnet", "args": [ "run", - "--rm", - "-i", - "mcp/sequentialthinking" + "--project", + "${input:consoleapp-project-path}" ] } } diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs index c7434a81..081e54d7 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs @@ -1,7 +1,11 @@ using McpSamples.OpenApiToSdk.HybridApp.Configurations; using McpSamples.Shared.Configurations; using McpSamples.Shared.Extensions; +using McpSamples.OpenApiToSdk.HybridApp.Services; +/// +/// Entry point for the OpenAPI to SDK MCP server. +/// var useStreamableHttp = AppSettings.UseStreamableHttp(Environment.GetEnvironmentVariables(), args); IHostApplicationBuilder builder = useStreamableHttp @@ -10,6 +14,10 @@ builder.Services.AddAppSettings(builder.Configuration, args); +// 추가: OpenAPI 서비스 등록 +builder.Services.AddHttpClient(); +builder.Services.AddScoped(); + IHost app = builder.BuildApp(useStreamableHttp); await app.RunAsync(); \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/IOpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/IOpenApiService.cs new file mode 100644 index 00000000..bb8377e4 --- /dev/null +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/IOpenApiService.cs @@ -0,0 +1,15 @@ +namespace McpSamples.OpenApiToSdk.HybridApp.Services; + +/// +/// This provides interfaces for OpenAPI service operations. +/// +public interface IOpenApiService +{ + /// + /// Downloads OpenAPI specification from a URL. + /// + /// The URL to download from. + /// Cancellation token. + /// The downloaded content as a string. + Task DownloadOpenApiSpecAsync(string url, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs new file mode 100644 index 00000000..ee3129e0 --- /dev/null +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs @@ -0,0 +1,30 @@ +namespace McpSamples.OpenApiToSdk.HybridApp.Services; + +/// +/// This represents the service for OpenAPI operations. +/// +/// instance. +/// instance. +public class OpenApiService(HttpClient httpClient, ILogger logger) : IOpenApiService +{ + /// + public async Task DownloadOpenApiSpecAsync(string url, CancellationToken cancellationToken = default) + { + try + { + logger.LogInformation("Downloading OpenAPI spec from {Url}", url); + var response = await httpClient.GetAsync(url, cancellationToken); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(cancellationToken); + logger.LogInformation("Successfully downloaded OpenAPI spec ({Length} characters)", content.Length); + + return content; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to download OpenAPI spec from {Url}", url); + throw; + } + } +} \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs index 3649ce22..7ad23a93 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs @@ -1,6 +1,37 @@ +using System.ComponentModel; + +using McpSamples.OpenApiToSdk.HybridApp.Configurations; +using McpSamples.OpenApiToSdk.HybridApp.Services; + +using ModelContextProtocol.Server; + namespace McpSamples.OpenApiToSdk.HybridApp.Tools; -public class OpenApiToSdkTool +/// +/// This provides interfaces for the OpenAPI to SDK tool. +/// +public interface IOpenApiToSdkTool { + /// + /// Downloads OpenAPI specification from a URL. + /// + /// The URL of the OpenAPI specification. + /// The downloaded OpenAPI specification content. + Task DownloadOpenApiSpecAsync(string openApiUrl); +} +/// +/// This represents the tool entity for OpenAPI to SDK operations. +/// +/// instance. +public class OpenApiToSdkTool(IOpenApiService openApiService) : IOpenApiToSdkTool +{ + /// + [McpServerTool(Name = "download_openapi_spec")] + [Description("Download OpenAPI specification from a URL")] + public async Task DownloadOpenApiSpecAsync( + [Description("URL of the OpenAPI specification")] string openApiUrl) + { + return await openApiService.DownloadOpenApiSpecAsync(openApiUrl); + } } \ No newline at end of file From 152a09db1e2691932ea563652f0351ea241ccc07 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Tue, 4 Nov 2025 01:12:54 +0900 Subject: [PATCH 14/61] Add Kiota SDK generation functionality and related methods --- .../Prompts/SdkGenerationPrompt.cs | 1 + .../Services/IOpenApiService.cs | 10 +++ .../Services/OpenApiService.cs | 64 ++++++++++++++++++- .../Tools/OpenApiToSdkTool.cs | 58 +++++++++++++++++ 4 files changed, 131 insertions(+), 2 deletions(-) diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs index 655ff1cb..af3472a6 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using ModelContextProtocol.Server; namespace McpSamples.OpenApiToSdk.HybridApp.Prompts; diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/IOpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/IOpenApiService.cs index bb8377e4..27da9573 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/IOpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/IOpenApiService.cs @@ -12,4 +12,14 @@ public interface IOpenApiService /// Cancellation token. /// The downloaded content as a string. Task DownloadOpenApiSpecAsync(string url, CancellationToken cancellationToken = default); + + /// + /// Runs Kiota CLI with the specified options. + /// + /// Path to the OpenAPI spec file. + /// Target language for the SDK. + /// Output directory for the generated SDK. + /// Additional Kiota options. + /// Error message if failed, null if successful. + Task RunKiotaAsync(string openApiSpecPath, string language, string outputDir, string? additionalOptions = null); } \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs index ee3129e0..162918ad 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs @@ -1,10 +1,11 @@ +using System.Diagnostics; +using System.Text; // Add this for StringBuilder + namespace McpSamples.OpenApiToSdk.HybridApp.Services; /// /// This represents the service for OpenAPI operations. /// -/// instance. -/// instance. public class OpenApiService(HttpClient httpClient, ILogger logger) : IOpenApiService { /// @@ -27,4 +28,63 @@ public async Task DownloadOpenApiSpecAsync(string url, CancellationToken throw; } } + + /// + public async Task RunKiotaAsync(string openApiSpecPath, string language, string outputDir, string? additionalOptions = null) + { + // Map Kiota command options + var arguments = new StringBuilder(); + arguments.Append($"generate --openapi \"{openApiSpecPath}\" --language {language} --output \"{outputDir}\""); + if (!string.IsNullOrWhiteSpace(additionalOptions)) + { + arguments.Append($" {additionalOptions}"); + } + + var processStartInfo = new ProcessStartInfo + { + FileName = "kiota", + Arguments = arguments.ToString(), + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + try + { + using var process = Process.Start(processStartInfo); + if (process == null) + { + return "Failed to start Kiota process."; + } + + // Asynchronous execution with timeout (5 minutes) + var timeout = TimeSpan.FromMinutes(5); + var outputTask = process.StandardOutput.ReadToEndAsync(); + var errorTask = process.StandardError.ReadToEndAsync(); + var exitTask = process.WaitForExitAsync(); + + if (await Task.WhenAny(exitTask, Task.Delay(timeout)) == exitTask) + { + await Task.WhenAll(outputTask, errorTask); + if (process.ExitCode != 0) + { + logger.LogError("Kiota execution failed: {Error}", await errorTask); + return $"Kiota error: {await errorTask}"; + } + logger.LogInformation("Kiota execution succeeded: {Output}", await outputTask); + return null; // Success + } + else + { + process.Kill(); + return "Kiota execution timed out."; + } + } + catch (Exception ex) + { + logger.LogError(ex, "Exception during Kiota execution"); + return $"Kiota exception: {ex.Message}"; + } + } } \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs index 7ad23a93..b495f534 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.IO; using McpSamples.OpenApiToSdk.HybridApp.Configurations; using McpSamples.OpenApiToSdk.HybridApp.Services; @@ -18,6 +19,15 @@ public interface IOpenApiToSdkTool /// The URL of the OpenAPI specification. /// The downloaded OpenAPI specification content. Task DownloadOpenApiSpecAsync(string openApiUrl); + + /// + /// Generates an SDK from an OpenAPI specification using Kiota. + /// + /// The URL of the OpenAPI specification. + /// The target language for the SDK (e.g., "csharp", "typescript"). + /// Optional output directory for the generated SDK. + /// The path to the generated SDK ZIP file or an error message. + Task GenerateSdkAsync(string openApiUrl, string language, string? outputDir = null); } /// @@ -34,4 +44,52 @@ public async Task DownloadOpenApiSpecAsync( { return await openApiService.DownloadOpenApiSpecAsync(openApiUrl); } + + /// + [McpServerTool(Name = "generate_sdk")] + [Description("Generate an SDK from an OpenAPI specification using Kiota")] + public async Task GenerateSdkAsync( + [Description("URL of the OpenAPI specification")] string openApiUrl, + [Description("Target language for the SDK (e.g., csharp, typescript)")] string language, + [Description("Optional output directory for the generated SDK")] string? outputDir = null) + { + // 1. Download OpenAPI spec + var specContent = await openApiService.DownloadOpenApiSpecAsync(openApiUrl); + if (string.IsNullOrEmpty(specContent)) + { + return "Failed to download OpenAPI specification."; + } + + // 2. Create project temp directory (openapi-to-sdk/temp/) + var projectRoot = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", "..")); + var projectTempDir = Path.Combine(projectRoot, "temp"); + Directory.CreateDirectory(projectTempDir); + + // 3. Save spec to temp file (Kiota requires file path) + var tempSpecPath = Path.Combine(projectTempDir, $"openapi-spec-{Guid.NewGuid()}.json"); + Console.WriteLine($"Temporary OpenAPI spec path: {tempSpecPath}"); + await File.WriteAllTextAsync(tempSpecPath, specContent); + + // 4. Set output directory (default: temp folder) + var finalOutputDir = outputDir ?? Path.Combine(projectTempDir, $"sdk-output-{Guid.NewGuid()}"); + Directory.CreateDirectory(finalOutputDir); + + // 4. Map Kiota command options + var additionalOptions = ""; // Additional options (e.g., --namespace MyNamespace) can be extended + var error = await openApiService.RunKiotaAsync(tempSpecPath, language, finalOutputDir, additionalOptions); + + // 5. Clean up temp file + File.Delete(tempSpecPath); + + if (!string.IsNullOrEmpty(error)) + { + return $"SDK generation failed: {error}"; + } + + // 6. Compress generated SDK to ZIP (refer to next task) + var zipPath = Path.Combine(projectTempDir, $"sdk-{Guid.NewGuid()}.zip"); + // TODO: Implement ZIP compression logic (add separate service) + + return $"SDK generated successfully. ZIP path: {zipPath}"; // Eventually return URI (Feature 1.2) + } } \ No newline at end of file From 50d97c6b3fb3ddc026197391f8873da1348ecbbc Mon Sep 17 00:00:00 2001 From: x-or-b Date: Tue, 4 Nov 2025 02:19:28 +0900 Subject: [PATCH 15/61] Add OpenAPI specification handling and SDK generation features - Introduced a new plan.md file outlining the features for OpenAPI-based SDK generation. - Enhanced OpenApiService to validate URLs and improve logging during OpenAPI spec downloads. - Updated OpenApiToSdkTool to handle additional options for SDK generation and improved directory management. - Implemented temporary file handling and ZIP compression for generated SDKs. --- openapi-to-sdk/plan.md | 83 ++++++++++++++++++ .../Services/OpenApiService.cs | 41 ++++++--- .../Tools/OpenApiToSdkTool.cs | 84 ++++++++++++++----- 3 files changed, 173 insertions(+), 35 deletions(-) create mode 100644 openapi-to-sdk/plan.md diff --git a/openapi-to-sdk/plan.md b/openapi-to-sdk/plan.md new file mode 100644 index 00000000..99905123 --- /dev/null +++ b/openapi-to-sdk/plan.md @@ -0,0 +1,83 @@ +### Epic: OpenAPI 기반 SDK 자동 생성 (MCP Server) + +- 사용자가 LLM 클라이언트를 통해 OpenAPI 명세를 제공하면, 원하는 언어의 SDK를 생성하고 받을 수 있는 MCP 서버 환경을 구축한다. + +--- + +### Feature 1: SDK 생성 및 반환 + +- **설명**: OpenAPI 명세를 받아 Kiota를 실행하고 결과를 압축한다. + +- User Story 1.1: 시스템으로서, 나는 OpenAPI 명세를 받아 SDK를 생성하고 압축하고 싶다. + + - **Tasks**: + - [ ] OpenAPI URL에서 명세 콘텐츠를 다운로드하거나, 전달받은 콘텐츠를 처리하는 모듈 구현. + - [ ] Kiota CLI 명령으로 옵션을 매핑하는 로직 구현. + - [ ] Process 클래스를 이용한 Kiota CLI 실행 래퍼 구현. + - [ ] 임시 폴더에 Kiota를 실행하여 SDK 소스 코드 생성. + - [ ] 생성된 SDK 폴더 전체를 하나의 ZIP 파일로 압축하는 로직 구현. + +- User Story 1.2: LLM 클라이언트로서, 나는 SDK 생성 결과를 받고 싶다. + - **Tasks**: + - [ ] LLM 클라이언트가 접근 가능한 URI ZIP 파일의 경로를 반환한다. + +--- + +### Feature 2: 오류 처리 및 피드백 + +- **설명**: SDK 생성 전 과정에서 발생하는 오류를 감지하고, 이를 구조화된 형태로 사용자에게 전달한다. + +- User Story 2.1: LLM 클라이언트로서, 나는 SDK 생성 실패 오류 메시지를 받고 싶다. + + - **Tasks**: + - [ ] 표준 오류 메시지의 JSON 구조 정의 (`{"errorCode": "string", "message": ...}`). + - [ ] 발생한 예외를 표준 오류 메시지로 변환 후, 적절한 응답 본문으로 반환. + +- User Story 2.2: 시스템으로서, 나는 Kiota 실행 오류를 구조화하여 처리하고 싶다. + - **Tasks**: + - [ ] Kiota CLI 실행 및 프로세스 관리를 위한 서비스 구현. + - [ ] Kiota 실행 결과 파싱 및 오류 매핑 로직 구현. + - [ ] 비동기 Kiota 실행 및 타임아웃 처리 구현. + +--- + +### Feature 3: LLM 연동 및 실행 지원 + +- **설명**: LLM 클라이언트가 서버의 기능을 쉽게 사용하도록 돕고, 다양한 환경에 MCP 서버를 실행할 수 있게 한다. + +- User Story 3.1: LLM 클라이언트로서, 나는 SDK 생성 기능을 쉽게 사용할 수 있도록 Pre-defined Prompt를 받고 싶다. + + - **Tasks**: + - [ ] SDK 생성 요청(Tools)을 위한 Pre-defined Prompt 정의. + +- User Story 3.2: 시스템으로서, 나는 MCP 서버를 초기화하고 서비스를 등록하고 싶다. + - **Tasks**: + - [ ] Program.cs에서 하이브리드 MCP 서버 초기화 구현 (STDIO/HTTP 모드 지원). + - [ ] OpenApiToSdkAppSettings 구성 및 DI 컨테이너 등록. + - [ ] Kiota 실행을 위한 서비스 등록 (HttpClient, 파일 처리 서비스). + - [ ] 프롬프트 및 도구 자동 등록을 위한 어셈블리 스캔 설정. + +--- + +### Feature 4: 인프라 및 배포 지원 + +- **설명**: 다양한 환경에서 서버를 실행하고 배포할 수 있도록 지원한다. + +- User Story 4.1: 개발자로서, 나는 로컬 개발 환경에서 서버를 실행하고 싶다. + + - **Tasks**: + - [ ] .vscode/mcp.stdio.local.json 및 mcp.http.local.json 설정 파일 작성. + - [ ] launchSettings.json에서 개발 환경 프로필 구성. + - [ ] appsettings.Development.json 작성. + +- User Story 4.2: 운영자로서, 나는 컨테이너 환경에서 서버를 배포하고 싶다. + + - **Tasks**: + - [ ] .vscode/mcp.stdio.container.json 및 mcp.http.container.json 설정 파일 작성. + - [ ] Dockerfile.openapi-to-sdk 작성. + +- User Story 4.3: 운영자로서, 나는 Azure 환경에서 서버를 배포하고 싶다. + - **Tasks**: + - [ ] .vscode/mcp.http.remote.json 설정 파일 작성. + - [ ] Azure Bicep 템플릿 (main.bicep, resources.bicep) 작성. + - [ ] azure.yaml 및 배포 스크립트 작성. diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs index 162918ad..ff015627 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs @@ -1,25 +1,32 @@ using System.Diagnostics; -using System.Text; // Add this for StringBuilder +using System.Text; namespace McpSamples.OpenApiToSdk.HybridApp.Services; /// /// This represents the service for OpenAPI operations. /// +/// The instance. +/// The instance. public class OpenApiService(HttpClient httpClient, ILogger logger) : IOpenApiService { /// public async Task DownloadOpenApiSpecAsync(string url, CancellationToken cancellationToken = default) { + if (string.IsNullOrWhiteSpace(url)) + { + throw new ArgumentException("URL is required.", nameof(url)); + } + try { logger.LogInformation("Downloading OpenAPI spec from {Url}", url); - var response = await httpClient.GetAsync(url, cancellationToken); + var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); - var content = await response.Content.ReadAsStringAsync(cancellationToken); - logger.LogInformation("Successfully downloaded OpenAPI spec ({Length} characters)", content.Length); - + var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + logger.LogInformation("Downloaded OpenAPI spec from {Url} (Length={Length})", url, content.Length); + return content; } catch (Exception ex) @@ -53,31 +60,41 @@ public async Task DownloadOpenApiSpecAsync(string url, CancellationToken try { using var process = Process.Start(processStartInfo); - if (process == null) + if (process is null) { return "Failed to start Kiota process."; } - // Asynchronous execution with timeout (5 minutes) + // Execute with timeout (5 minutes) var timeout = TimeSpan.FromMinutes(5); var outputTask = process.StandardOutput.ReadToEndAsync(); var errorTask = process.StandardError.ReadToEndAsync(); var exitTask = process.WaitForExitAsync(); - if (await Task.WhenAny(exitTask, Task.Delay(timeout)) == exitTask) + if (await Task.WhenAny(exitTask, Task.Delay(timeout)).ConfigureAwait(false) == exitTask) { - await Task.WhenAll(outputTask, errorTask); + await Task.WhenAll(outputTask, errorTask).ConfigureAwait(false); + if (process.ExitCode != 0) { - logger.LogError("Kiota execution failed: {Error}", await errorTask); + logger.LogError("Kiota execution failed (ExitCode={ExitCode}): {Error}", process.ExitCode, await errorTask); return $"Kiota error: {await errorTask}"; } + logger.LogInformation("Kiota execution succeeded: {Output}", await outputTask); - return null; // Success + return null; } else { - process.Kill(); + try + { + process.Kill(entireProcessTree: true); + } + catch + { + // Best effort cleanup + } + return "Kiota execution timed out."; } } diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs index b495f534..80737537 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs @@ -1,7 +1,6 @@ using System.ComponentModel; -using System.IO; +using System.IO.Compression; -using McpSamples.OpenApiToSdk.HybridApp.Configurations; using McpSamples.OpenApiToSdk.HybridApp.Services; using ModelContextProtocol.Server; @@ -24,10 +23,10 @@ public interface IOpenApiToSdkTool /// Generates an SDK from an OpenAPI specification using Kiota. /// /// The URL of the OpenAPI specification. - /// The target language for the SDK (e.g., "csharp", "typescript"). - /// Optional output directory for the generated SDK. + /// The target language for the SDK. + /// Additional Kiota CLI options. /// The path to the generated SDK ZIP file or an error message. - Task GenerateSdkAsync(string openApiUrl, string language, string? outputDir = null); + Task GenerateSdkAsync(string openApiUrl, string language, string? additionalOptions = null); } /// @@ -51,45 +50,84 @@ public async Task DownloadOpenApiSpecAsync( public async Task GenerateSdkAsync( [Description("URL of the OpenAPI specification")] string openApiUrl, [Description("Target language for the SDK (e.g., csharp, typescript)")] string language, - [Description("Optional output directory for the generated SDK")] string? outputDir = null) + [Description("Optional extra Kiota options (e.g., --namespace Contoso.Api)")] string? additionalOptions = null) { - // 1. Download OpenAPI spec + // Download OpenAPI spec from URL var specContent = await openApiService.DownloadOpenApiSpecAsync(openApiUrl); - if (string.IsNullOrEmpty(specContent)) + if (string.IsNullOrWhiteSpace(specContent)) { return "Failed to download OpenAPI specification."; } - // 2. Create project temp directory (openapi-to-sdk/temp/) - var projectRoot = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", "..")); + // Resolve openapi-to-sdk root directory and create temp folder + var projectRoot = ResolveOpenApiToSdkRoot(Directory.GetCurrentDirectory()); var projectTempDir = Path.Combine(projectRoot, "temp"); Directory.CreateDirectory(projectTempDir); - // 3. Save spec to temp file (Kiota requires file path) - var tempSpecPath = Path.Combine(projectTempDir, $"openapi-spec-{Guid.NewGuid()}.json"); - Console.WriteLine($"Temporary OpenAPI spec path: {tempSpecPath}"); + // Save spec to temporary file (Kiota requires file path) + var tempSpecPath = Path.Combine(projectTempDir, $"openapi-spec-{Guid.NewGuid():N}.json"); await File.WriteAllTextAsync(tempSpecPath, specContent); - // 4. Set output directory (default: temp folder) - var finalOutputDir = outputDir ?? Path.Combine(projectTempDir, $"sdk-output-{Guid.NewGuid()}"); + // Create output directory for generated SDK + var finalOutputDir = Path.Combine(projectTempDir, $"sdk-output-{Guid.NewGuid():N}"); Directory.CreateDirectory(finalOutputDir); - // 4. Map Kiota command options - var additionalOptions = ""; // Additional options (e.g., --namespace MyNamespace) can be extended + // Execute Kiota with mapped options var error = await openApiService.RunKiotaAsync(tempSpecPath, language, finalOutputDir, additionalOptions); - // 5. Clean up temp file - File.Delete(tempSpecPath); + // Clean up temporary spec file + try + { + File.Delete(tempSpecPath); + } + catch + { + // Best effort cleanup + } if (!string.IsNullOrEmpty(error)) { return $"SDK generation failed: {error}"; } - // 6. Compress generated SDK to ZIP (refer to next task) - var zipPath = Path.Combine(projectTempDir, $"sdk-{Guid.NewGuid()}.zip"); - // TODO: Implement ZIP compression logic (add separate service) + // Compress generated SDK to ZIP + var zipPath = Path.Combine(projectTempDir, $"sdk-{Guid.NewGuid():N}.zip"); + if (File.Exists(zipPath)) + { + File.Delete(zipPath); + } + + ZipFile.CreateFromDirectory(finalOutputDir, zipPath); + + return $"SDK generated successfully. ZIP path: {zipPath}"; + } + + /// + /// Resolves the openapi-to-sdk project root directory. + /// + /// The starting directory path. + /// The resolved project root directory path. + private static string ResolveOpenApiToSdkRoot(string start) + { + var dir = start; + while (!string.IsNullOrEmpty(dir)) + { + var name = Path.GetFileName(dir.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + if (string.Equals(name, "openapi-to-sdk", StringComparison.OrdinalIgnoreCase)) + { + return dir; + } + + var parent = Directory.GetParent(dir); + if (parent is null) + { + break; + } + + dir = parent.FullName; + } - return $"SDK generated successfully. ZIP path: {zipPath}"; // Eventually return URI (Feature 1.2) + // Fallback to current directory if root not found + return start; } } \ No newline at end of file From aa07d5a2144f75cb1262fafa1bda090279c57da9 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Tue, 4 Nov 2025 03:06:53 +0900 Subject: [PATCH 16/61] Refactor SdkGenerationPrompt to standardize prompt name for SDK generation --- .../Prompts/SdkGenerationPrompt.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs index af3472a6..689a1add 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs @@ -1,4 +1,5 @@ using System.ComponentModel; + using ModelContextProtocol.Server; namespace McpSamples.OpenApiToSdk.HybridApp.Prompts; @@ -32,7 +33,7 @@ public interface ISdkGenerationPrompt public class SdkGenerationPrompt : ISdkGenerationPrompt { /// - [McpServerPrompt(Name = "generate_sdk_with_parsing", Title = "Generate SDK from OpenAPI Spec with Kiota Parsing")] + [McpServerPrompt(Name = "generate_sdk", Title = "Generate SDK from OpenAPI Spec with Kiota Parsing")] [Description("Provides a structured prompt for parsing Kiota options and generating an SDK.")] public string GetSdkGenerationPrompt( [Description("The Location of the OpenAPI description.")] string openApiDocUrl, From 802736a743c4aedeb7b86661fb34ff08eb960fde Mon Sep 17 00:00:00 2001 From: x-or-b Date: Mon, 10 Nov 2025 21:39:19 +0900 Subject: [PATCH 17/61] Refactor server configuration and enhance OpenAPI service registration in Program.cs; update OpenApiToSdkTool for improved functionality --- openapi-to-sdk/.vscode/mcp.http.local.json | 2 +- .../Program.cs | 29 +++++++++++++++---- .../Tools/OpenApiToSdkTool.cs | 5 +++- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/openapi-to-sdk/.vscode/mcp.http.local.json b/openapi-to-sdk/.vscode/mcp.http.local.json index da096d0a..8e5caa3f 100644 --- a/openapi-to-sdk/.vscode/mcp.http.local.json +++ b/openapi-to-sdk/.vscode/mcp.http.local.json @@ -1,6 +1,6 @@ { "servers": { - "todo-list": { + "openapi-to-sdk": { "type": "http", "url": "http://localhost:5222/mcp" } diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs index 081e54d7..4d3ca8ad 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs @@ -1,11 +1,10 @@ using McpSamples.OpenApiToSdk.HybridApp.Configurations; +using McpSamples.OpenApiToSdk.HybridApp.Services; +using McpSamples.OpenApiToSdk.HybridApp.Tools; using McpSamples.Shared.Configurations; using McpSamples.Shared.Extensions; -using McpSamples.OpenApiToSdk.HybridApp.Services; +using McpSamples.Shared.OpenApi; -/// -/// Entry point for the OpenAPI to SDK MCP server. -/// var useStreamableHttp = AppSettings.UseStreamableHttp(Environment.GetEnvironmentVariables(), args); IHostApplicationBuilder builder = useStreamableHttp @@ -14,10 +13,30 @@ builder.Services.AddAppSettings(builder.Configuration, args); -// 추가: OpenAPI 서비스 등록 builder.Services.AddHttpClient(); builder.Services.AddScoped(); +builder.Services.AddScoped(); + +if (useStreamableHttp == true) +{ + builder.Services.AddHttpContextAccessor(); + builder.Services.AddOpenApi("swagger", o => + { + o.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0; + o.AddDocumentTransformer>(); + }); + builder.Services.AddOpenApi("openapi", o => + { + o.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0; + o.AddDocumentTransformer>(); + }); +} IHost app = builder.BuildApp(useStreamableHttp); +if (useStreamableHttp == true) +{ + (app as WebApplication)!.MapOpenApi("/{documentName}.json"); +} + await app.RunAsync(); \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs index 80737537..83cd5d35 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs @@ -1,5 +1,8 @@ +using System; using System.ComponentModel; +using System.IO; using System.IO.Compression; +using System.Threading.Tasks; using McpSamples.OpenApiToSdk.HybridApp.Services; @@ -96,7 +99,7 @@ public async Task GenerateSdkAsync( { File.Delete(zipPath); } - + ZipFile.CreateFromDirectory(finalOutputDir, zipPath); return $"SDK generated successfully. ZIP path: {zipPath}"; From 3624873ed4814aef46c4ea9ee9f9f1b443aa974a Mon Sep 17 00:00:00 2001 From: x-or-b Date: Tue, 11 Nov 2025 01:07:52 +0900 Subject: [PATCH 18/61] Refactor OpenApiToSdkTool to return structured results; update .gitignore and configuration files --- .gitignore | 2 - openapi-to-sdk/.vscode/mcp.http.local.json | 2 +- .../McpSamples.OpenApiToSdk.HybridApp.csproj | 4 - .../Models/DownloadResult.cs | 17 ++ .../Models/OpenApiToSdkResult.cs | 18 ++ .../Program.cs | 14 +- .../Tools/OpenApiToSdkTool.cs | 162 ++++++++++-------- .../wwwroot/.gitignore | 2 + 8 files changed, 138 insertions(+), 83 deletions(-) create mode 100644 openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Models/DownloadResult.cs create mode 100644 openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Models/OpenApiToSdkResult.cs create mode 100644 openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/wwwroot/.gitignore diff --git a/.gitignore b/.gitignore index bbdc6d7c..e7fa1686 100644 --- a/.gitignore +++ b/.gitignore @@ -34,8 +34,6 @@ bld/ # Visual Studio 2015/2017 cache/options directory .vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ diff --git a/openapi-to-sdk/.vscode/mcp.http.local.json b/openapi-to-sdk/.vscode/mcp.http.local.json index 8e5caa3f..da6b22a7 100644 --- a/openapi-to-sdk/.vscode/mcp.http.local.json +++ b/openapi-to-sdk/.vscode/mcp.http.local.json @@ -2,7 +2,7 @@ "servers": { "openapi-to-sdk": { "type": "http", - "url": "http://localhost:5222/mcp" + "url": "http://0.0.0.0:5222/mcp" } } } \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/McpSamples.OpenApiToSdk.HybridApp.csproj b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/McpSamples.OpenApiToSdk.HybridApp.csproj index cb485ff2..eceed2b6 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/McpSamples.OpenApiToSdk.HybridApp.csproj +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/McpSamples.OpenApiToSdk.HybridApp.csproj @@ -13,10 +13,6 @@ 8b94c6d9-feae-4416-8f70-4b50ea51170b - - - - diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Models/DownloadResult.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Models/DownloadResult.cs new file mode 100644 index 00000000..99296045 --- /dev/null +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Models/DownloadResult.cs @@ -0,0 +1,17 @@ +namespace McpSamples.OpenApiToSdk.HybridApp.Models; + +/// +/// Represents the result of a download operation. +/// +public class DownloadResult +{ + /// + /// Gets or sets the downloaded content. + /// + public string? Content { get; set; } + + /// + /// Gets or sets the error message if the operation failed. + /// + public string? ErrorMessage { get; set; } +} diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Models/OpenApiToSdkResult.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Models/OpenApiToSdkResult.cs new file mode 100644 index 00000000..157fae8f --- /dev/null +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Models/OpenApiToSdkResult.cs @@ -0,0 +1,18 @@ +namespace McpSamples.OpenApiToSdk.HybridApp.Models; + +/// +/// Represents the result of an SDK generation operation. +/// +public class OpenApiToSdkResult +{ + /// + /// Gets or sets the path to the generated ZIP file. + /// This will be a local file path. + /// + public string? ZipPath { get; set; } + + /// + /// Gets or sets the error message if the operation failed. + /// + public string? ErrorMessage { get; set; } +} diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs index 4d3ca8ad..f8e6d9b3 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs @@ -1,9 +1,11 @@ using McpSamples.OpenApiToSdk.HybridApp.Configurations; using McpSamples.OpenApiToSdk.HybridApp.Services; -using McpSamples.OpenApiToSdk.HybridApp.Tools; +using McpSamples.OpenApiToSdk.HybridApp.Tools; // Added for OpenApiToSdkTool using McpSamples.Shared.Configurations; using McpSamples.Shared.Extensions; -using McpSamples.Shared.OpenApi; +using McpSamples.Shared.OpenApi; // Added for OpenAPI + +using Microsoft.OpenApi.Models; // Added for OpenApiSpecVersion var useStreamableHttp = AppSettings.UseStreamableHttp(Environment.GetEnvironmentVariables(), args); @@ -13,13 +15,12 @@ builder.Services.AddAppSettings(builder.Configuration, args); -builder.Services.AddHttpClient(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddHttpClient(); +builder.Services.AddScoped(); // Re-added explicit registration if (useStreamableHttp == true) { - builder.Services.AddHttpContextAccessor(); + builder.Services.AddHttpContextAccessor(); // Re-added builder.Services.AddOpenApi("swagger", o => { o.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0; @@ -37,6 +38,7 @@ if (useStreamableHttp == true) { (app as WebApplication)!.MapOpenApi("/{documentName}.json"); + (app as WebApplication)!.UseStaticFiles(); // Re-added } await app.RunAsync(); \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs index 83cd5d35..bf92bc22 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs @@ -4,8 +4,13 @@ using System.IO.Compression; using System.Threading.Tasks; +using McpSamples.OpenApiToSdk.HybridApp.Models; using McpSamples.OpenApiToSdk.HybridApp.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + using ModelContextProtocol.Server; namespace McpSamples.OpenApiToSdk.HybridApp.Tools; @@ -19,118 +24,135 @@ public interface IOpenApiToSdkTool /// Downloads OpenAPI specification from a URL. /// /// The URL of the OpenAPI specification. - /// The downloaded OpenAPI specification content. - Task DownloadOpenApiSpecAsync(string openApiUrl); + /// A containing the downloaded content or an error message. + Task DownloadOpenApiSpecAsync(string openApiUrl); /// /// Generates an SDK from an OpenAPI specification using Kiota. /// /// The URL of the OpenAPI specification. - /// The target language for the SDK. + /// The target language for the SDK (e.g., "csharp", "typescript"). /// Additional Kiota CLI options. - /// The path to the generated SDK ZIP file or an error message. - Task GenerateSdkAsync(string openApiUrl, string language, string? additionalOptions = null); + /// An containing the path to the generated SDK ZIP file or an error message. + Task GenerateSdkAsync(string openApiUrl, string language, string? additionalOptions = null); } /// /// This represents the tool entity for OpenAPI to SDK operations. /// -/// instance. -public class OpenApiToSdkTool(IOpenApiService openApiService) : IOpenApiToSdkTool +[McpServerToolType] +public class OpenApiToSdkTool : IOpenApiToSdkTool { + private readonly IOpenApiService _openApiService; + private readonly ILogger _logger; + private readonly IHostEnvironment _hostEnvironment; + private readonly IHttpContextAccessor _httpContextAccessor; + + public OpenApiToSdkTool(IOpenApiService openApiService, ILogger logger, IHostEnvironment hostEnvironment, IHttpContextAccessor httpContextAccessor) + { + _openApiService = openApiService ?? throw new ArgumentNullException(nameof(openApiService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _hostEnvironment = hostEnvironment ?? throw new ArgumentNullException(nameof(hostEnvironment)); + _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); + } + /// [McpServerTool(Name = "download_openapi_spec")] [Description("Download OpenAPI specification from a URL")] - public async Task DownloadOpenApiSpecAsync( + public async Task DownloadOpenApiSpecAsync( [Description("URL of the OpenAPI specification")] string openApiUrl) { - return await openApiService.DownloadOpenApiSpecAsync(openApiUrl); + var result = new DownloadResult(); + try + { + result.Content = await _openApiService.DownloadOpenApiSpecAsync(openApiUrl); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error downloading OpenAPI spec from {Url}", openApiUrl); + result.ErrorMessage = ex.Message; + } + return result; } /// [McpServerTool(Name = "generate_sdk")] [Description("Generate an SDK from an OpenAPI specification using Kiota")] - public async Task GenerateSdkAsync( + public async Task GenerateSdkAsync( [Description("URL of the OpenAPI specification")] string openApiUrl, [Description("Target language for the SDK (e.g., csharp, typescript)")] string language, [Description("Optional extra Kiota options (e.g., --namespace Contoso.Api)")] string? additionalOptions = null) { - // Download OpenAPI spec from URL - var specContent = await openApiService.DownloadOpenApiSpecAsync(openApiUrl); - if (string.IsNullOrWhiteSpace(specContent)) + var result = new OpenApiToSdkResult(); + var tempSpecPath = string.Empty; + var sdkOutputDir = string.Empty; + var tempZipPath = string.Empty; + + try { - return "Failed to download OpenAPI specification."; - } + var specContent = await _openApiService.DownloadOpenApiSpecAsync(openApiUrl); + if (string.IsNullOrWhiteSpace(specContent)) + { + result.ErrorMessage = "Failed to download or empty OpenAPI specification."; + return result; + } - // Resolve openapi-to-sdk root directory and create temp folder - var projectRoot = ResolveOpenApiToSdkRoot(Directory.GetCurrentDirectory()); - var projectTempDir = Path.Combine(projectRoot, "temp"); - Directory.CreateDirectory(projectTempDir); + var tempDir = Path.GetTempPath(); + tempSpecPath = Path.Combine(tempDir, $"openapi-spec-{Guid.NewGuid():N}.json"); + await File.WriteAllTextAsync(tempSpecPath, specContent); - // Save spec to temporary file (Kiota requires file path) - var tempSpecPath = Path.Combine(projectTempDir, $"openapi-spec-{Guid.NewGuid():N}.json"); - await File.WriteAllTextAsync(tempSpecPath, specContent); + sdkOutputDir = Path.Combine(tempDir, $"sdk-output-{Guid.NewGuid():N}"); + Directory.CreateDirectory(sdkOutputDir); - // Create output directory for generated SDK - var finalOutputDir = Path.Combine(projectTempDir, $"sdk-output-{Guid.NewGuid():N}"); - Directory.CreateDirectory(finalOutputDir); + var error = await _openApiService.RunKiotaAsync(tempSpecPath, language, sdkOutputDir, additionalOptions); + if (!string.IsNullOrEmpty(error)) + { + result.ErrorMessage = $"SDK generation failed: {error}"; + return result; + } - // Execute Kiota with mapped options - var error = await openApiService.RunKiotaAsync(tempSpecPath, language, finalOutputDir, additionalOptions); + // Prepare public directory for the zip file + var downloadsDir = Path.Combine(_hostEnvironment.ContentRootPath, "wwwroot", "generated-sdks"); + Directory.CreateDirectory(downloadsDir); + var zipFileName = $"{language}-{DateTime.Now:yyyyMMddHHmmss}.zip"; // Changed to language-datetime.zip + var finalZipPath = Path.Combine(downloadsDir, zipFileName); - // Clean up temporary spec file - try - { - File.Delete(tempSpecPath); - } - catch - { - // Best effort cleanup - } + // Compress generated SDK to ZIP + ZipFile.CreateFromDirectory(sdkOutputDir, finalZipPath); - if (!string.IsNullOrEmpty(error)) - { - return $"SDK generation failed: {error}"; + // Construct downloadable URI + if (_httpContextAccessor.HttpContext is not null) + { + var request = _httpContextAccessor.HttpContext.Request; + var baseUrl = $"{request.Scheme}://{request.Host}"; + result.ZipPath = $"{baseUrl}/generated-sdks/{zipFileName}"; + } + else + { + // Fallback to local file path for non-HTTP contexts (e.g., STDIO) + result.ZipPath = finalZipPath; + } } - - // Compress generated SDK to ZIP - var zipPath = Path.Combine(projectTempDir, $"sdk-{Guid.NewGuid():N}.zip"); - if (File.Exists(zipPath)) + catch (Exception ex) { - File.Delete(zipPath); + _logger.LogError(ex, "An exception occurred during SDK generation."); + result.ErrorMessage = ex.Message; } - - ZipFile.CreateFromDirectory(finalOutputDir, zipPath); - - return $"SDK generated successfully. ZIP path: {zipPath}"; - } - - /// - /// Resolves the openapi-to-sdk project root directory. - /// - /// The starting directory path. - /// The resolved project root directory path. - private static string ResolveOpenApiToSdkRoot(string start) - { - var dir = start; - while (!string.IsNullOrEmpty(dir)) + finally { - var name = Path.GetFileName(dir.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); - if (string.Equals(name, "openapi-to-sdk", StringComparison.OrdinalIgnoreCase)) + // Cleanup temporary files and directories + if (!string.IsNullOrEmpty(tempSpecPath) && File.Exists(tempSpecPath)) { - return dir; + File.Delete(tempSpecPath); } - - var parent = Directory.GetParent(dir); - if (parent is null) + if (!string.IsNullOrEmpty(sdkOutputDir) && Directory.Exists(sdkOutputDir)) { - break; + Directory.Delete(sdkOutputDir, true); } - - dir = parent.FullName; + // The finalZipPath is in wwwroot, so it's not cleaned up here. + // It's meant to be served. } - // Fallback to current directory if root not found - return start; + return result; } } \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/wwwroot/.gitignore b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/wwwroot/.gitignore new file mode 100644 index 00000000..57d1c066 --- /dev/null +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/wwwroot/.gitignore @@ -0,0 +1,2 @@ +# Ignore generated SDKs +/generated-sdks/ From 1b32d108a19cb727188500a162ffcbee8513153a Mon Sep 17 00:00:00 2001 From: x-or-b Date: Tue, 11 Nov 2025 01:49:03 +0900 Subject: [PATCH 19/61] Update server URL in mcp.http.local.json to use localhost for local development --- openapi-to-sdk/.vscode/mcp.http.local.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi-to-sdk/.vscode/mcp.http.local.json b/openapi-to-sdk/.vscode/mcp.http.local.json index da6b22a7..8e5caa3f 100644 --- a/openapi-to-sdk/.vscode/mcp.http.local.json +++ b/openapi-to-sdk/.vscode/mcp.http.local.json @@ -2,7 +2,7 @@ "servers": { "openapi-to-sdk": { "type": "http", - "url": "http://0.0.0.0:5222/mcp" + "url": "http://localhost:5222/mcp" } } } \ No newline at end of file From 8ba0612127b8ce1c79c71d3c655ccb4d7f7e99fe Mon Sep 17 00:00:00 2001 From: x-or-b Date: Tue, 11 Nov 2025 02:06:26 +0900 Subject: [PATCH 20/61] Cleaned up comments in the code. --- .../src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs index f8e6d9b3..09ee609a 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs @@ -1,11 +1,11 @@ using McpSamples.OpenApiToSdk.HybridApp.Configurations; using McpSamples.OpenApiToSdk.HybridApp.Services; -using McpSamples.OpenApiToSdk.HybridApp.Tools; // Added for OpenApiToSdkTool +using McpSamples.OpenApiToSdk.HybridApp.Tools; using McpSamples.Shared.Configurations; using McpSamples.Shared.Extensions; -using McpSamples.Shared.OpenApi; // Added for OpenAPI +using McpSamples.Shared.OpenApi; -using Microsoft.OpenApi.Models; // Added for OpenApiSpecVersion +using Microsoft.OpenApi.Models; var useStreamableHttp = AppSettings.UseStreamableHttp(Environment.GetEnvironmentVariables(), args); @@ -16,11 +16,11 @@ builder.Services.AddAppSettings(builder.Configuration, args); builder.Services.AddHttpClient(); -builder.Services.AddScoped(); // Re-added explicit registration +builder.Services.AddScoped(); if (useStreamableHttp == true) { - builder.Services.AddHttpContextAccessor(); // Re-added + builder.Services.AddHttpContextAccessor(); builder.Services.AddOpenApi("swagger", o => { o.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0; @@ -38,7 +38,7 @@ if (useStreamableHttp == true) { (app as WebApplication)!.MapOpenApi("/{documentName}.json"); - (app as WebApplication)!.UseStaticFiles(); // Re-added + (app as WebApplication)!.UseStaticFiles(); } await app.RunAsync(); \ No newline at end of file From 11747fdeff02657015638abcab2051cd20f9a517 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Tue, 11 Nov 2025 02:53:50 +0900 Subject: [PATCH 21/61] Add Dockerfiles, configuration files, and infrastructure templates for OpenAPI to SDK integration --- Dockerfile.openapi-to-sdk | 26 ++++ Dockerfile.openapi-to-sdk-azure | 20 +++ openapi-to-sdk/.dockerignore | 32 +++++ openapi-to-sdk/.gitignore | 2 + .../.vscode/mcp.http.container.json | 8 ++ openapi-to-sdk/.vscode/mcp.http.remote.json | 15 ++ .../.vscode/mcp.stdio.container.json | 14 ++ openapi-to-sdk/azure.yaml | 16 +++ openapi-to-sdk/infra/abbreviations.json | 136 ++++++++++++++++++ openapi-to-sdk/infra/main.bicep | 47 ++++++ openapi-to-sdk/infra/main.parameters.json | 18 +++ .../infra/modules/fetch-container-image.bicep | 8 ++ openapi-to-sdk/infra/resources.bicep | 134 +++++++++++++++++ 13 files changed, 476 insertions(+) create mode 100644 Dockerfile.openapi-to-sdk create mode 100644 Dockerfile.openapi-to-sdk-azure create mode 100644 openapi-to-sdk/.dockerignore create mode 100644 openapi-to-sdk/.gitignore create mode 100644 openapi-to-sdk/.vscode/mcp.http.container.json create mode 100644 openapi-to-sdk/.vscode/mcp.http.remote.json create mode 100644 openapi-to-sdk/.vscode/mcp.stdio.container.json create mode 100644 openapi-to-sdk/azure.yaml create mode 100644 openapi-to-sdk/infra/abbreviations.json create mode 100644 openapi-to-sdk/infra/main.bicep create mode 100644 openapi-to-sdk/infra/main.parameters.json create mode 100644 openapi-to-sdk/infra/modules/fetch-container-image.bicep create mode 100644 openapi-to-sdk/infra/resources.bicep diff --git a/Dockerfile.openapi-to-sdk b/Dockerfile.openapi-to-sdk new file mode 100644 index 00000000..f7af9708 --- /dev/null +++ b/Dockerfile.openapi-to-sdk @@ -0,0 +1,26 @@ +# 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 ./openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp /source/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp + +WORKDIR /source/openapi-to-sdk/src/McpSamples.OpenApiToSdk.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 . + +USER $APP_UID + +ENTRYPOINT ["dotnet", "McpSamples.OpenApiToSdk.HybridApp.dll"] diff --git a/Dockerfile.openapi-to-sdk-azure b/Dockerfile.openapi-to-sdk-azure new file mode 100644 index 00000000..6b290bd7 --- /dev/null +++ b/Dockerfile.openapi-to-sdk-azure @@ -0,0 +1,20 @@ +# syntax=docker/dockerfile:1 + +FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build + +COPY ./shared/McpSamples.Shared /source/shared/McpSamples.Shared +COPY ./openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp /source/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp + +WORKDIR /source/openapi-to-sdk/src/McpSamples.OpenApiToSdk.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 . + +USER $APP_UID + +ENTRYPOINT ["dotnet", "McpSamples.OpenApiToSdk.HybridApp.dll"] diff --git a/openapi-to-sdk/.dockerignore b/openapi-to-sdk/.dockerignore new file mode 100644 index 00000000..9e03c484 --- /dev/null +++ b/openapi-to-sdk/.dockerignore @@ -0,0 +1,32 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.DS_Store +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose.y*ml +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/openapi-to-sdk/.gitignore b/openapi-to-sdk/.gitignore new file mode 100644 index 00000000..e45d8342 --- /dev/null +++ b/openapi-to-sdk/.gitignore @@ -0,0 +1,2 @@ +!.vscode/mcp.json +.azure diff --git a/openapi-to-sdk/.vscode/mcp.http.container.json b/openapi-to-sdk/.vscode/mcp.http.container.json new file mode 100644 index 00000000..c52e811a --- /dev/null +++ b/openapi-to-sdk/.vscode/mcp.http.container.json @@ -0,0 +1,8 @@ +{ + "servers": { + "openapi-to-sdk": { + "type": "http", + "url": "http://localhost:8080/mcp" + } + } +} \ No newline at end of file diff --git a/openapi-to-sdk/.vscode/mcp.http.remote.json b/openapi-to-sdk/.vscode/mcp.http.remote.json new file mode 100644 index 00000000..4eb240be --- /dev/null +++ b/openapi-to-sdk/.vscode/mcp.http.remote.json @@ -0,0 +1,15 @@ +{ + "inputs": [ + { + "type": "promptString", + "id": "acaapp-server-fqdn", + "description": "Azure Container Apps FQDN" + } + ], + "servers": { + "openapi-to-sdk": { + "type": "http", + "url": "https://${input:acaapp-server-fqdn}/mcp" + } + } +} \ No newline at end of file diff --git a/openapi-to-sdk/.vscode/mcp.stdio.container.json b/openapi-to-sdk/.vscode/mcp.stdio.container.json new file mode 100644 index 00000000..c201f2af --- /dev/null +++ b/openapi-to-sdk/.vscode/mcp.stdio.container.json @@ -0,0 +1,14 @@ +{ + "servers": { + "openapi-to-sdk": { + "type": "stdio", + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "ghcr.io/microsoft/mcp-dotnet-samples/openapi-to-sdk:latest" + ] + } + } +} \ No newline at end of file diff --git a/openapi-to-sdk/azure.yaml b/openapi-to-sdk/azure.yaml new file mode 100644 index 00000000..b0e9b4b0 --- /dev/null +++ b/openapi-to-sdk/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: openapi-to-sdk + +metadata: + template: azd-init@1.14.0 + +services: + openapi-to-sdk: + project: src/McpSamples.OpenApiToSdk.HybridApp + host: containerapp + language: dotnet + docker: + path: ../../../Dockerfile.openapi-to-sdk-azure + context: ../../../ + remoteBuild: true diff --git a/openapi-to-sdk/infra/abbreviations.json b/openapi-to-sdk/infra/abbreviations.json new file mode 100644 index 00000000..1533dee5 --- /dev/null +++ b/openapi-to-sdk/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/openapi-to-sdk/infra/main.bicep b/openapi-to-sdk/infra/main.bicep new file mode 100644 index 00000000..0aa17db7 --- /dev/null +++ b/openapi-to-sdk/infra/main.bicep @@ -0,0 +1,47 @@ +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 mcpOpenApiToSdkExists 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 + mcpOpenApiToSdkExists: mcpOpenApiToSdkExists + } +} + +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT +output AZURE_RESOURCE_MCP_OPENAPI_TO_SDK_ID string = resources.outputs.AZURE_RESOURCE_MCP_OPENAPI_TO_SDK_ID +output AZURE_RESOURCE_MCP_OPENAPI_TO_SDK_NAME string = resources.outputs.AZURE_RESOURCE_MCP_OPENAPI_TO_SDK_NAME +output AZURE_RESOURCE_MCP_OPENAPI_TO_SDK_FQDN string = resources.outputs.AZURE_RESOURCE_MCP_OPENAPI_TO_SDK_FQDN diff --git a/openapi-to-sdk/infra/main.parameters.json b/openapi-to-sdk/infra/main.parameters.json new file mode 100644 index 00000000..05d30fab --- /dev/null +++ b/openapi-to-sdk/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}" + }, + "mcpOpenApiToSdkExists": { + "value": "${SERVICE_MCP_OPENAPI_TO_SDK_RESOURCE_EXISTS=false}" + }, + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + } + } +} diff --git a/openapi-to-sdk/infra/modules/fetch-container-image.bicep b/openapi-to-sdk/infra/modules/fetch-container-image.bicep new file mode 100644 index 00000000..78d1e7ee --- /dev/null +++ b/openapi-to-sdk/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/openapi-to-sdk/infra/resources.bicep b/openapi-to-sdk/infra/resources.bicep new file mode 100644 index 00000000..55ab5575 --- /dev/null +++ b/openapi-to-sdk/infra/resources.bicep @@ -0,0 +1,134 @@ +@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 mcpOpenApiToSdkExists bool + +@description('Id of the user or app to assign application roles') +param principalId string + +var abbrs = loadJsonContent('./abbreviations.json') +var resourceToken = uniqueString(subscription().id, resourceGroup().id, location) + +// 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 + } +} + +// 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: mcpOpenApiToSdkIdentity.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.4.5' = { + name: 'container-apps-environment' + params: { + logAnalyticsWorkspaceResourceId: monitoring.outputs.logAnalyticsWorkspaceResourceId + name: '${abbrs.appManagedEnvironments}${resourceToken}' + location: location + zoneRedundant: false + } +} + +// User assigned identity +module mcpOpenApiToSdkIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { + name: 'mcpOpenApiToSdkIdentity' + params: { + name: '${abbrs.managedIdentityUserAssignedIdentities}mcpopenapitosdk-${resourceToken}' + location: location + } +} + +// Azure Container Apps +module mcpOpenApiToSdkFetchLatestImage './modules/fetch-container-image.bicep' = { + name: 'mcpOpenApiToSdk-fetch-image' + params: { + exists: mcpOpenApiToSdkExists + name: 'openapi-to-sdk' + } +} + +module mcpOpenApiToSdk 'br/public:avm/res/app/container-app:0.8.0' = { + name: 'mcpOpenApiToSdk' + params: { + name: 'openapi-to-sdk' + ingressTargetPort: 8080 + scaleMinReplicas: 1 + scaleMaxReplicas: 10 + secrets: { + secureList: [ + ] + } + containers: [ + { + image: mcpOpenApiToSdkFetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' + name: 'main' + resources: { + cpu: json('0.5') + memory: '1.0Gi' + } + args: [ + '--http' + ] + env: [ + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: monitoring.outputs.applicationInsightsConnectionString + } + { + name: 'AZURE_CLIENT_ID' + value: mcpOpenApiToSdkIdentity.outputs.clientId + } + { + name: 'PORT' + value: '8080' + } + ] + } + ] + managedIdentities: { + systemAssigned: false + userAssignedResourceIds: [ + mcpOpenApiToSdkIdentity.outputs.resourceId + ] + } + registries: [ + { + server: containerRegistry.outputs.loginServer + identity: mcpOpenApiToSdkIdentity.outputs.resourceId + } + ] + environmentResourceId: containerAppsEnvironment.outputs.resourceId + location: location + tags: union(tags, { 'azd-service-name': 'openapi-to-sdk' }) + } +} + +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer +output AZURE_RESOURCE_MCP_OPENAPI_TO_SDK_ID string = mcpOpenApiToSdk.outputs.resourceId +output AZURE_RESOURCE_MCP_OPENAPI_TO_SDK_NAME string = mcpOpenApiToSdk.outputs.name +output AZURE_RESOURCE_MCP_OPENAPI_TO_SDK_FQDN string = mcpOpenApiToSdk.outputs.fqdn From 17d9c98485a50d61a0810fca7ee1f8aeb5589bc7 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Tue, 11 Nov 2025 21:17:59 +0900 Subject: [PATCH 22/61] Refactor DownloadOpenApiSpecAsync method to simplify error handling and improve logging --- .../Services/OpenApiService.cs | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs index ff015627..4ccb9d8d 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs @@ -18,22 +18,14 @@ public async Task DownloadOpenApiSpecAsync(string url, CancellationToken throw new ArgumentException("URL is required.", nameof(url)); } - try - { - logger.LogInformation("Downloading OpenAPI spec from {Url}", url); - var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); + logger.LogInformation("Downloading OpenAPI spec from {Url}", url); + var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); - var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - logger.LogInformation("Downloaded OpenAPI spec from {Url} (Length={Length})", url, content.Length); - - return content; - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to download OpenAPI spec from {Url}", url); - throw; - } + var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + logger.LogInformation("Downloaded OpenAPI spec from {Url} (Length={Length})", url, content.Length); + + return content; } /// From 5dfe5fc1c0b3e79205ad9188844e2a6d239a491a Mon Sep 17 00:00:00 2001 From: x-or-b Date: Sun, 16 Nov 2025 23:38:13 +0900 Subject: [PATCH 23/61] Enhance OpenApiToSdkTool for improved SDK generation and path handling --- Dockerfile.openapi-to-sdk | 8 +++ .../.vscode/mcp.stdio.container.json | 2 +- openapi-to-sdk/TODO | 0 .../Program.cs | 1 - .../Services/OpenApiService.cs | 8 +-- .../Tools/OpenApiToSdkTool.cs | 53 ++++++++++++------- .../wwwroot/.gitignore | 2 - 7 files changed, 47 insertions(+), 27 deletions(-) create mode 100644 openapi-to-sdk/TODO delete mode 100644 openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/wwwroot/.gitignore diff --git a/Dockerfile.openapi-to-sdk b/Dockerfile.openapi-to-sdk index f7af9708..9925509f 100644 --- a/Dockerfile.openapi-to-sdk +++ b/Dockerfile.openapi-to-sdk @@ -2,6 +2,9 @@ FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build +# Install Kiota CLI +RUN dotnet tool install --global Microsoft.OpenApi.Kiota + COPY ./shared/McpSamples.Shared /source/shared/McpSamples.Shared COPY ./openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp /source/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp @@ -19,8 +22,13 @@ FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final WORKDIR /app +# Copy app from build stage COPY --from=build /app . +# Copy the entire Kiota tool directory to a neutral location and add it to the PATH +COPY --from=build /root/.dotnet/tools /tools/ +ENV PATH="/tools:${PATH}" + USER $APP_UID ENTRYPOINT ["dotnet", "McpSamples.OpenApiToSdk.HybridApp.dll"] diff --git a/openapi-to-sdk/.vscode/mcp.stdio.container.json b/openapi-to-sdk/.vscode/mcp.stdio.container.json index c201f2af..54444335 100644 --- a/openapi-to-sdk/.vscode/mcp.stdio.container.json +++ b/openapi-to-sdk/.vscode/mcp.stdio.container.json @@ -7,7 +7,7 @@ "run", "-i", "--rm", - "ghcr.io/microsoft/mcp-dotnet-samples/openapi-to-sdk:latest" + "openapi-to-sdk:latest" ] } } diff --git a/openapi-to-sdk/TODO b/openapi-to-sdk/TODO new file mode 100644 index 00000000..e69de29b diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs index 09ee609a..52d48643 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs @@ -16,7 +16,6 @@ builder.Services.AddAppSettings(builder.Configuration, args); builder.Services.AddHttpClient(); -builder.Services.AddScoped(); if (useStreamableHttp == true) { diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs index 4ccb9d8d..947ec42a 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs @@ -24,7 +24,7 @@ public async Task DownloadOpenApiSpecAsync(string url, CancellationToken var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); logger.LogInformation("Downloaded OpenAPI spec from {Url} (Length={Length})", url, content.Length); - + return content; } @@ -66,13 +66,13 @@ public async Task DownloadOpenApiSpecAsync(string url, CancellationToken if (await Task.WhenAny(exitTask, Task.Delay(timeout)).ConfigureAwait(false) == exitTask) { await Task.WhenAll(outputTask, errorTask).ConfigureAwait(false); - + if (process.ExitCode != 0) { logger.LogError("Kiota execution failed (ExitCode={ExitCode}): {Error}", process.ExitCode, await errorTask); return $"Kiota error: {await errorTask}"; } - + logger.LogInformation("Kiota execution succeeded: {Output}", await outputTask); return null; } @@ -86,7 +86,7 @@ public async Task DownloadOpenApiSpecAsync(string url, CancellationToken { // Best effort cleanup } - + return "Kiota execution timed out."; } } diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs index bf92bc22..b14461dc 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs @@ -33,8 +33,9 @@ public interface IOpenApiToSdkTool /// The URL of the OpenAPI specification. /// The target language for the SDK (e.g., "csharp", "typescript"). /// Additional Kiota CLI options. + /// Optional: The directory where the generated SDK ZIP file will be saved. If not provided, a default 'GeneratedSDKs' folder will be used. /// An containing the path to the generated SDK ZIP file or an error message. - Task GenerateSdkAsync(string openApiUrl, string language, string? additionalOptions = null); + Task GenerateSdkAsync(string openApiUrl, string language, string? additionalOptions = null, string? outputDir = null); } /// @@ -45,15 +46,15 @@ public class OpenApiToSdkTool : IOpenApiToSdkTool { private readonly IOpenApiService _openApiService; private readonly ILogger _logger; - private readonly IHostEnvironment _hostEnvironment; - private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IHostEnvironment? _hostEnvironment; + private readonly IHttpContextAccessor? _httpContextAccessor; - public OpenApiToSdkTool(IOpenApiService openApiService, ILogger logger, IHostEnvironment hostEnvironment, IHttpContextAccessor httpContextAccessor) + public OpenApiToSdkTool(IOpenApiService openApiService, ILogger logger, IHostEnvironment? hostEnvironment = null, IHttpContextAccessor? httpContextAccessor = null) { _openApiService = openApiService ?? throw new ArgumentNullException(nameof(openApiService)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _hostEnvironment = hostEnvironment ?? throw new ArgumentNullException(nameof(hostEnvironment)); - _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); + _hostEnvironment = hostEnvironment; + _httpContextAccessor = httpContextAccessor; } /// @@ -81,12 +82,12 @@ public async Task DownloadOpenApiSpecAsync( public async Task GenerateSdkAsync( [Description("URL of the OpenAPI specification")] string openApiUrl, [Description("Target language for the SDK (e.g., csharp, typescript)")] string language, - [Description("Optional extra Kiota options (e.g., --namespace Contoso.Api)")] string? additionalOptions = null) + [Description("Optional extra Kiota options (e.g., --namespace Contoso.Api)")] string? additionalOptions = null, + [Description("Optional: The directory where the generated SDK ZIP file will be saved. If not provided, a default 'GeneratedSDKs' folder will be used.")] string? outputDir = null) { var result = new OpenApiToSdkResult(); var tempSpecPath = string.Empty; var sdkOutputDir = string.Empty; - var tempZipPath = string.Empty; try { @@ -111,25 +112,41 @@ public async Task GenerateSdkAsync( return result; } - // Prepare public directory for the zip file - var downloadsDir = Path.Combine(_hostEnvironment.ContentRootPath, "wwwroot", "generated-sdks"); - Directory.CreateDirectory(downloadsDir); - var zipFileName = $"{language}-{DateTime.Now:yyyyMMddHHmmss}.zip"; // Changed to language-datetime.zip - var finalZipPath = Path.Combine(downloadsDir, zipFileName); + string finalOutputDirectory; + string zipFileName = $"{language}-{DateTime.Now:yyyyMMddHHmmss}.zip"; + + // HTTP 모드: wwwroot/generated에 저장 + if (_httpContextAccessor?.HttpContext is not null && _hostEnvironment is not null) + { + finalOutputDirectory = Path.Combine(_hostEnvironment.ContentRootPath, "wwwroot", "generated"); + } + // STDIO 모드 또는 사용자가 경로를 지정하지 않은 경우: 현재 작업 디렉터리에 저장 + else + { + finalOutputDirectory = Path.Combine(Directory.GetCurrentDirectory(), "generated"); + } + + // 사용자가 outputDir을 지정한 경우, 해당 경로를 사용 + if (!string.IsNullOrWhiteSpace(outputDir)) + { + finalOutputDirectory = outputDir; + } + + Directory.CreateDirectory(finalOutputDirectory); + string finalZipPath = Path.Combine(finalOutputDirectory, zipFileName); // Compress generated SDK to ZIP ZipFile.CreateFromDirectory(sdkOutputDir, finalZipPath); - // Construct downloadable URI - if (_httpContextAccessor.HttpContext is not null) + // HTTP 모드에서는 URI를, 그렇지 않으면 로컬 경로를 반환 + if (_httpContextAccessor?.HttpContext is not null && _hostEnvironment is not null) { var request = _httpContextAccessor.HttpContext.Request; var baseUrl = $"{request.Scheme}://{request.Host}"; - result.ZipPath = $"{baseUrl}/generated-sdks/{zipFileName}"; + result.ZipPath = $"{baseUrl}/generated/{zipFileName}"; } else { - // Fallback to local file path for non-HTTP contexts (e.g., STDIO) result.ZipPath = finalZipPath; } } @@ -149,8 +166,6 @@ public async Task GenerateSdkAsync( { Directory.Delete(sdkOutputDir, true); } - // The finalZipPath is in wwwroot, so it's not cleaned up here. - // It's meant to be served. } return result; diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/wwwroot/.gitignore b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/wwwroot/.gitignore deleted file mode 100644 index 57d1c066..00000000 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/wwwroot/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Ignore generated SDKs -/generated-sdks/ From 8b8a1cc7be0656571b58e54640b3a3d527b5e0d7 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Sun, 16 Nov 2025 23:40:08 +0900 Subject: [PATCH 24/61] Update output directory description and comments for clarity in OpenApiToSdkTool --- .../Tools/OpenApiToSdkTool.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs index b14461dc..d82ea620 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs @@ -83,7 +83,7 @@ public async Task GenerateSdkAsync( [Description("URL of the OpenAPI specification")] string openApiUrl, [Description("Target language for the SDK (e.g., csharp, typescript)")] string language, [Description("Optional extra Kiota options (e.g., --namespace Contoso.Api)")] string? additionalOptions = null, - [Description("Optional: The directory where the generated SDK ZIP file will be saved. If not provided, a default 'GeneratedSDKs' folder will be used.")] string? outputDir = null) + [Description("Optional: The directory where the generated SDK ZIP file will be saved. If not provided, a default 'generated' folder will be used.")] string? outputDir = null) { var result = new OpenApiToSdkResult(); var tempSpecPath = string.Empty; @@ -115,18 +115,18 @@ public async Task GenerateSdkAsync( string finalOutputDirectory; string zipFileName = $"{language}-{DateTime.Now:yyyyMMddHHmmss}.zip"; - // HTTP 모드: wwwroot/generated에 저장 + // HTTP: wwwroot/generated/ if (_httpContextAccessor?.HttpContext is not null && _hostEnvironment is not null) { finalOutputDirectory = Path.Combine(_hostEnvironment.ContentRootPath, "wwwroot", "generated"); } - // STDIO 모드 또는 사용자가 경로를 지정하지 않은 경우: 현재 작업 디렉터리에 저장 + // STDIO, 사용자가 경로를 지정하지 않은 경우: generated/ else { finalOutputDirectory = Path.Combine(Directory.GetCurrentDirectory(), "generated"); } - // 사용자가 outputDir을 지정한 경우, 해당 경로를 사용 + // 사용자가 outputDir을 지정한 경우 if (!string.IsNullOrWhiteSpace(outputDir)) { finalOutputDirectory = outputDir; From 54a37931c8c66ad06e1d4b0d4ce19652814feee9 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Mon, 17 Nov 2025 00:03:35 +0900 Subject: [PATCH 25/61] Enhance SDK generation prompts and options in OpenApiToSdkTool - Updated ISdkGenerationPrompt interface to include optional className and namespaceName parameters. - Modified GetSdkGenerationPrompt method to handle new parameters and provide default values. - Improved GenerateSdkAsync method in OpenApiToSdkTool to support className and namespaceName options for Kiota CLI. - Refactored command argument construction for clarity and maintainability. --- .../Prompts/SdkGenerationPrompt.cs | 31 ++++++------------- .../Services/OpenApiService.cs | 4 ++- .../Tools/OpenApiToSdkTool.cs | 23 +++++++++++--- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs index 689a1add..bb727d00 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs @@ -14,9 +14,11 @@ public interface ISdkGenerationPrompt /// /// The location of the OpenAPI description. /// The target language for the SDK. + /// Optional: The class name to use for the core client class. + /// Optional: The namespace to use for the core client class. /// Additional user-provided options for Kiota. /// A formatted prompt for SDK generation with parsing instructions. - string GetSdkGenerationPrompt(string openApiDocUrl, string language, string? additionalOptions = null); + string GetSdkGenerationPrompt(string openApiDocUrl, string language, string? className = null, string? namespaceName = null, string? additionalOptions = null); /// /// Gets a prompt for validating an OpenAPI specification. @@ -38,40 +40,25 @@ public class SdkGenerationPrompt : ISdkGenerationPrompt public string GetSdkGenerationPrompt( [Description("The Location of the OpenAPI description.")] string openApiDocUrl, [Description("The target language for the SDK.")] string language, - [Description("Additional options for Kiota (e.g., '--namespace-name MyNamespace').")] string? additionalOptions = null) + [Description("Optional: The class name to use for the core client class. Defaults to ApiClient.")] string? className = null, + [Description("Optional: The namespace to use for the core client class. Defaults to ApiSdk.")] string? namespaceName = null, + [Description("Additional options for Kiota (e.g., '--include-path Paths').")] string? additionalOptions = null) { return $""" Generate an SDK from the provided OpenAPI specification using Kiota. OpenAPI Location: {openApiDocUrl} Language: {language} + Class Name: {className ?? "Default (ApiClient)"} + Namespace: {namespaceName ?? "Default (ApiSdk)"} Additional Options: {additionalOptions ?? "None"} Instructions: - - Parse the additional options to valid Kiota command-line arguments. + - Parse the options to valid Kiota command-line arguments. - Use the 'generate_sdk' tool with the parsed options to process the spec. - Validate the OpenAPI spec before generation. - Return the ZIP file URI upon success. - Handle errors gracefully and provide feedback. """; } - - /// - [McpServerPrompt(Name = "validate_openapi_spec", Title = "Validate OpenAPI Specification")] - [Description("Provides a structured prompt for validating an OpenAPI specification before SDK generation.")] - public string GetValidationPrompt( - [Description("The OpenAPI specification URL.")] string openApiDocUrl) - { - return $""" - Validate the provided OpenAPI specification. - - OpenAPI Location: {openApiDocUrl} - - Instructions: - - Download and parse the OpenAPI spec. - - Check for syntax errors, missing required fields, and schema validity. - - Report any validation issues or confirm validity. - - Use this validation before proceeding to SDK generation. - """; - } } \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs index 947ec42a..ff04a21c 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs @@ -33,12 +33,14 @@ public async Task DownloadOpenApiSpecAsync(string url, CancellationToken { // Map Kiota command options var arguments = new StringBuilder(); - arguments.Append($"generate --openapi \"{openApiSpecPath}\" --language {language} --output \"{outputDir}\""); + arguments.Append($"generate"); + arguments.Append($" --openapi \"{openApiSpecPath}\" --language {language} --output \"{outputDir}\""); if (!string.IsNullOrWhiteSpace(additionalOptions)) { arguments.Append($" {additionalOptions}"); } + var processStartInfo = new ProcessStartInfo { FileName = "kiota", diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs index d82ea620..920cc91f 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.IO; using System.IO.Compression; +using System.Text; using System.Threading.Tasks; using McpSamples.OpenApiToSdk.HybridApp.Models; @@ -32,10 +33,12 @@ public interface IOpenApiToSdkTool /// /// The URL of the OpenAPI specification. /// The target language for the SDK (e.g., "csharp", "typescript"). + /// Optional: The class name to use for the core client class. + /// Optional: The namespace to use for the core client class. /// Additional Kiota CLI options. - /// Optional: The directory where the generated SDK ZIP file will be saved. If not provided, a default 'GeneratedSDKs' folder will be used. + /// Optional: The directory where the generated SDK ZIP file will be saved. If not provided, a default 'generated' folder will be used. /// An containing the path to the generated SDK ZIP file or an error message. - Task GenerateSdkAsync(string openApiUrl, string language, string? additionalOptions = null, string? outputDir = null); + Task GenerateSdkAsync(string openApiUrl, string language, string? className = null, string? namespaceName = null, string? additionalOptions = null, string? outputDir = null); } /// @@ -82,7 +85,9 @@ public async Task DownloadOpenApiSpecAsync( public async Task GenerateSdkAsync( [Description("URL of the OpenAPI specification")] string openApiUrl, [Description("Target language for the SDK (e.g., csharp, typescript)")] string language, - [Description("Optional extra Kiota options (e.g., --namespace Contoso.Api)")] string? additionalOptions = null, + [Description("Optional: The class name to use for the core client class. Defaults to ApiClient.")] string? className = null, + [Description("Optional: The namespace to use for the core client class. Defaults to ApiSdk.")] string? namespaceName = null, + [Description("Optional extra Kiota options (e.g., --include-path Paths)")] string? additionalOptions = null, [Description("Optional: The directory where the generated SDK ZIP file will be saved. If not provided, a default 'generated' folder will be used.")] string? outputDir = null) { var result = new OpenApiToSdkResult(); @@ -105,7 +110,17 @@ public async Task GenerateSdkAsync( sdkOutputDir = Path.Combine(tempDir, $"sdk-output-{Guid.NewGuid():N}"); Directory.CreateDirectory(sdkOutputDir); - var error = await _openApiService.RunKiotaAsync(tempSpecPath, language, sdkOutputDir, additionalOptions); + var kiotaOptions = new StringBuilder(additionalOptions ?? ""); + if (!string.IsNullOrWhiteSpace(className)) + { + kiotaOptions.Append($" --class-name {className}"); + } + if (!string.IsNullOrWhiteSpace(namespaceName)) + { + kiotaOptions.Append($" --namespace-name {namespaceName}"); + } + + var error = await _openApiService.RunKiotaAsync(tempSpecPath, language, sdkOutputDir, kiotaOptions.ToString().Trim()); if (!string.IsNullOrEmpty(error)) { result.ErrorMessage = $"SDK generation failed: {error}"; From 32daaa5eb1f37611339a04bfdedfcbe4b3241f3a Mon Sep 17 00:00:00 2001 From: x-or-b Date: Tue, 18 Nov 2025 00:46:48 +0900 Subject: [PATCH 26/61] Refactor Dockerfile and SdkGenerationPrompt for improved clarity and organization --- Dockerfile.openapi-to-sdk | 24 +++++++++---------- .../Prompts/SdkGenerationPrompt.cs | 11 ++------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/Dockerfile.openapi-to-sdk b/Dockerfile.openapi-to-sdk index 9925509f..3bcabc36 100644 --- a/Dockerfile.openapi-to-sdk +++ b/Dockerfile.openapi-to-sdk @@ -2,33 +2,31 @@ FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build -# Install Kiota CLI -RUN dotnet tool install --global Microsoft.OpenApi.Kiota - COPY ./shared/McpSamples.Shared /source/shared/McpSamples.Shared COPY ./openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp /source/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp WORKDIR /source/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp +RUN dotnet tool install --global Microsoft.OpenApi.Kiota + 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 + "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 app from build stage COPY --from=build /app . -# Copy the entire Kiota tool directory to a neutral location and add it to the PATH -COPY --from=build /root/.dotnet/tools /tools/ -ENV PATH="/tools:${PATH}" +COPY --from=build /root/.dotnet/tools /opt/kiota-tools +ENV PATH="/opt/kiota-tools:${PATH}" +RUN chmod -R +rX /opt/kiota-tools USER $APP_UID -ENTRYPOINT ["dotnet", "McpSamples.OpenApiToSdk.HybridApp.dll"] +ENTRYPOINT ["dotnet", "McpSamples.OpenApiToSdk.HybridApp.dll"] \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs index bb727d00..04953ea2 100644 --- a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs +++ b/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs @@ -19,13 +19,6 @@ public interface ISdkGenerationPrompt /// Additional user-provided options for Kiota. /// A formatted prompt for SDK generation with parsing instructions. string GetSdkGenerationPrompt(string openApiDocUrl, string language, string? className = null, string? namespaceName = null, string? additionalOptions = null); - - /// - /// Gets a prompt for validating an OpenAPI specification. - /// - /// The URL of the OpenAPI specification. - /// A formatted prompt for validating the OpenAPI spec. - string GetValidationPrompt(string openApiDocUrl); } /// @@ -40,8 +33,8 @@ public class SdkGenerationPrompt : ISdkGenerationPrompt public string GetSdkGenerationPrompt( [Description("The Location of the OpenAPI description.")] string openApiDocUrl, [Description("The target language for the SDK.")] string language, - [Description("Optional: The class name to use for the core client class. Defaults to ApiClient.")] string? className = null, - [Description("Optional: The namespace to use for the core client class. Defaults to ApiSdk.")] string? namespaceName = null, + [Description("The class name to use for the core client class. Defaults to ApiClient.")] string? className = null, + [Description("The namespace to use for the core client class. Defaults to ApiSdk.")] string? namespaceName = null, [Description("Additional options for Kiota (e.g., '--include-path Paths').")] string? additionalOptions = null) { return $""" From 593098886c56598cb9c9f6d03e1e2f73c5df3a61 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Tue, 18 Nov 2025 01:00:12 +0900 Subject: [PATCH 27/61] Rename folder and file from APItoSDK to ApiToSdk --- .../Configurations/OpenApiToSdkAppSettings.cs} | 0 .../McpSamples.OpenApiToSdk.HybridApp.csproj | 0 .../Models/DownloadResult.cs | 0 .../Models/OpenApiToSdkResult.cs | 0 .../Program.cs | 0 .../Prompts/SdkGenerationPrompt.cs | 0 .../Properties/launchSettings.json | 0 .../Services/IOpenApiService.cs | 0 .../Services/OpenApiService.cs | 0 .../Tools/OpenApiToSdkTool.cs | 0 .../appsettings.Development.json | 0 .../appsettings.json | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename openapi-to-sdk/src/{McpSamples.OpenAPItoSDK.HybridApp/Configurations/OpenAPItoSDKAppSettings.cs => McpSamples.OpenApiToSdk.HybridApp/Configurations/OpenApiToSdkAppSettings.cs} (100%) rename openapi-to-sdk/src/{McpSamples.OpenAPItoSDK.HybridApp => McpSamples.OpenApiToSdk.HybridApp}/McpSamples.OpenApiToSdk.HybridApp.csproj (100%) rename openapi-to-sdk/src/{McpSamples.OpenAPItoSDK.HybridApp => McpSamples.OpenApiToSdk.HybridApp}/Models/DownloadResult.cs (100%) rename openapi-to-sdk/src/{McpSamples.OpenAPItoSDK.HybridApp => McpSamples.OpenApiToSdk.HybridApp}/Models/OpenApiToSdkResult.cs (100%) rename openapi-to-sdk/src/{McpSamples.OpenAPItoSDK.HybridApp => McpSamples.OpenApiToSdk.HybridApp}/Program.cs (100%) rename openapi-to-sdk/src/{McpSamples.OpenAPItoSDK.HybridApp => McpSamples.OpenApiToSdk.HybridApp}/Prompts/SdkGenerationPrompt.cs (100%) rename openapi-to-sdk/src/{McpSamples.OpenAPItoSDK.HybridApp => McpSamples.OpenApiToSdk.HybridApp}/Properties/launchSettings.json (100%) rename openapi-to-sdk/src/{McpSamples.OpenAPItoSDK.HybridApp => McpSamples.OpenApiToSdk.HybridApp}/Services/IOpenApiService.cs (100%) rename openapi-to-sdk/src/{McpSamples.OpenAPItoSDK.HybridApp => McpSamples.OpenApiToSdk.HybridApp}/Services/OpenApiService.cs (100%) rename openapi-to-sdk/src/{McpSamples.OpenAPItoSDK.HybridApp => McpSamples.OpenApiToSdk.HybridApp}/Tools/OpenApiToSdkTool.cs (100%) rename openapi-to-sdk/src/{McpSamples.OpenAPItoSDK.HybridApp => McpSamples.OpenApiToSdk.HybridApp}/appsettings.Development.json (100%) rename openapi-to-sdk/src/{McpSamples.OpenAPItoSDK.HybridApp => McpSamples.OpenApiToSdk.HybridApp}/appsettings.json (100%) diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Configurations/OpenAPItoSDKAppSettings.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Configurations/OpenApiToSdkAppSettings.cs similarity index 100% rename from openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Configurations/OpenAPItoSDKAppSettings.cs rename to openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Configurations/OpenApiToSdkAppSettings.cs diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/McpSamples.OpenApiToSdk.HybridApp.csproj b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/McpSamples.OpenApiToSdk.HybridApp.csproj similarity index 100% rename from openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/McpSamples.OpenApiToSdk.HybridApp.csproj rename to openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/McpSamples.OpenApiToSdk.HybridApp.csproj diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Models/DownloadResult.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Models/DownloadResult.cs similarity index 100% rename from openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Models/DownloadResult.cs rename to openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Models/DownloadResult.cs diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Models/OpenApiToSdkResult.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Models/OpenApiToSdkResult.cs similarity index 100% rename from openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Models/OpenApiToSdkResult.cs rename to openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Models/OpenApiToSdkResult.cs diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs similarity index 100% rename from openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Program.cs rename to openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs similarity index 100% rename from openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Prompts/SdkGenerationPrompt.cs rename to openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Properties/launchSettings.json b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Properties/launchSettings.json similarity index 100% rename from openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Properties/launchSettings.json rename to openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Properties/launchSettings.json diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/IOpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/IOpenApiService.cs similarity index 100% rename from openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/IOpenApiService.cs rename to openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/IOpenApiService.cs diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs similarity index 100% rename from openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Services/OpenApiService.cs rename to openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs similarity index 100% rename from openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/Tools/OpenApiToSdkTool.cs rename to openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/appsettings.Development.json b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/appsettings.Development.json similarity index 100% rename from openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/appsettings.Development.json rename to openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/appsettings.Development.json diff --git a/openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/appsettings.json b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/appsettings.json similarity index 100% rename from openapi-to-sdk/src/McpSamples.OpenAPItoSDK.HybridApp/appsettings.json rename to openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/appsettings.json From 1af61831a074631d0a7411102b2bb66a7f129f65 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Tue, 18 Nov 2025 01:33:58 +0900 Subject: [PATCH 28/61] Change Readme file --- openapi-to-sdk/README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openapi-to-sdk/README.md b/openapi-to-sdk/README.md index c973295f..877a8706 100644 --- a/openapi-to-sdk/README.md +++ b/openapi-to-sdk/README.md @@ -1,3 +1,11 @@ # MCP Server: Awesome Copilot -This is an MCP server that integrates with [Kiota](https://github.com/microsoft/kiota) to generate an SDK from OpenAPI documents. \ No newline at end of file +This is an MCP server that integrates with [Kiota](https://github.com/microsoft/kiota) to generate an SDK from OpenAPI documents. + +```bash + docker run -i --rm -p 8080:8080 -v "$(pwd)/output:/app/generated" openapi-to-sdk:latest +``` + +```bash + docker run -i --rm -p 8080:8080 -v "$(pwd)/output:/app/wwwroot/generated" openapi-to-sdk:latest +``` \ No newline at end of file From 45f43cb237963f2a3453e30d73ce4289443b169e Mon Sep 17 00:00:00 2001 From: x-or-b Date: Tue, 18 Nov 2025 08:50:18 +0900 Subject: [PATCH 29/61] revise dockerfile --- Dockerfile.openapi-to-sdk | 8 +++++++- openapi-to-sdk/.gitignore | 2 ++ openapi-to-sdk/README.md | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Dockerfile.openapi-to-sdk b/Dockerfile.openapi-to-sdk index 3bcabc36..cc0c0e1c 100644 --- a/Dockerfile.openapi-to-sdk +++ b/Dockerfile.openapi-to-sdk @@ -24,8 +24,14 @@ WORKDIR /app COPY --from=build /app . COPY --from=build /root/.dotnet/tools /opt/kiota-tools +RUN chmod -R 755 /opt/kiota-tools && \ + ln -s /opt/kiota-tools/kiota /usr/local/bin/kiota + +RUN mkdir -p /app/generated /wwwroot /wwwroot/generated && \ + chmod 755 /app/generated && \ + chmod -R 777 /wwwroot + ENV PATH="/opt/kiota-tools:${PATH}" -RUN chmod -R +rX /opt/kiota-tools USER $APP_UID diff --git a/openapi-to-sdk/.gitignore b/openapi-to-sdk/.gitignore index e45d8342..b9006ccf 100644 --- a/openapi-to-sdk/.gitignore +++ b/openapi-to-sdk/.gitignore @@ -1,2 +1,4 @@ !.vscode/mcp.json .azure + +image.png \ No newline at end of file diff --git a/openapi-to-sdk/README.md b/openapi-to-sdk/README.md index 877a8706..0df0e8b6 100644 --- a/openapi-to-sdk/README.md +++ b/openapi-to-sdk/README.md @@ -3,9 +3,9 @@ This is an MCP server that integrates with [Kiota](https://github.com/microsoft/kiota) to generate an SDK from OpenAPI documents. ```bash - docker run -i --rm -p 8080:8080 -v "$(pwd)/output:/app/generated" openapi-to-sdk:latest + docker run -i --rm -p 8080:8080 -v "$(pwd)/generated:/app/generated" openapi-to-sdk:latest ``` ```bash - docker run -i --rm -p 8080:8080 -v "$(pwd)/output:/app/wwwroot/generated" openapi-to-sdk:latest + docker run -i --rm -p 8080:8080 -v "$(pwd)/generated:/app/wwwroot/generated" openapi-to-sdk:latest --http ``` \ No newline at end of file From 64de2420881878836a0e89e11f898706c83c8fbd Mon Sep 17 00:00:00 2001 From: x-or-b Date: Tue, 18 Nov 2025 17:40:18 +0900 Subject: [PATCH 30/61] Refactor Dockerfile to improve directory creation and permission settings --- Dockerfile.openapi-to-sdk | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile.openapi-to-sdk b/Dockerfile.openapi-to-sdk index cc0c0e1c..96f49877 100644 --- a/Dockerfile.openapi-to-sdk +++ b/Dockerfile.openapi-to-sdk @@ -27,9 +27,10 @@ COPY --from=build /root/.dotnet/tools /opt/kiota-tools RUN chmod -R 755 /opt/kiota-tools && \ ln -s /opt/kiota-tools/kiota /usr/local/bin/kiota -RUN mkdir -p /app/generated /wwwroot /wwwroot/generated && \ - chmod 755 /app/generated && \ - chmod -R 777 /wwwroot +# /app/generated 및 /wwwroot, /wwwroot/generated 디렉터리 생성 및 권한 설정 +RUN mkdir -p /app/generated /app/wwwroot/generated && \ + chown -R $APP_UID:$APP_UID /app/generated /app/wwwroot && \ + chmod -R 777 /app/generated /app/wwwroot ENV PATH="/opt/kiota-tools:${PATH}" From c75adf007f590d1b645146fe189160e2d81fc26d Mon Sep 17 00:00:00 2001 From: x-or-b Date: Tue, 18 Nov 2025 23:26:35 +0900 Subject: [PATCH 31/61] Refactor Dockerfile to streamline directory creation and permission settings --- Dockerfile.openapi-to-sdk | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Dockerfile.openapi-to-sdk b/Dockerfile.openapi-to-sdk index 96f49877..12299f4b 100644 --- a/Dockerfile.openapi-to-sdk +++ b/Dockerfile.openapi-to-sdk @@ -11,9 +11,9 @@ RUN dotnet tool install --global Microsoft.OpenApi.Kiota ARG TARGETARCH RUN case "$TARGETARCH" in \ - "amd64") RID="linux-musl-x64" ;; \ - "arm64") RID="linux-musl-arm64" ;; \ - *) RID="linux-musl-x64" ;; \ + "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 @@ -27,13 +27,12 @@ COPY --from=build /root/.dotnet/tools /opt/kiota-tools RUN chmod -R 755 /opt/kiota-tools && \ ln -s /opt/kiota-tools/kiota /usr/local/bin/kiota -# /app/generated 및 /wwwroot, /wwwroot/generated 디렉터리 생성 및 권한 설정 -RUN mkdir -p /app/generated /app/wwwroot/generated && \ - chown -R $APP_UID:$APP_UID /app/generated /app/wwwroot && \ - chmod -R 777 /app/generated /app/wwwroot - -ENV PATH="/opt/kiota-tools:${PATH}" +RUN chown $APP_UID /app USER $APP_UID +RUN mkdir -p /app/generated /app/wwwroot/generated + +ENV PATH="/opt/kiota-tools:${PATH}" + ENTRYPOINT ["dotnet", "McpSamples.OpenApiToSdk.HybridApp.dll"] \ No newline at end of file From 52ec0d9f6308123a69dce6476d5117e6f501d6d4 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Sat, 22 Nov 2025 19:50:42 +0900 Subject: [PATCH 32/61] Refactor Dockerfile and OpenApiToSdkTool to streamline directory creation --- Dockerfile.openapi-to-sdk | 2 +- .../Tools/OpenApiToSdkTool.cs | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Dockerfile.openapi-to-sdk b/Dockerfile.openapi-to-sdk index 12299f4b..ba3294b1 100644 --- a/Dockerfile.openapi-to-sdk +++ b/Dockerfile.openapi-to-sdk @@ -31,7 +31,7 @@ RUN chown $APP_UID /app USER $APP_UID -RUN mkdir -p /app/generated /app/wwwroot/generated +RUN mkdir -p /app/wwwroot/generated ENV PATH="/opt/kiota-tools:${PATH}" diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs index 920cc91f..9c6a24db 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs @@ -130,18 +130,10 @@ public async Task GenerateSdkAsync( string finalOutputDirectory; string zipFileName = $"{language}-{DateTime.Now:yyyyMMddHHmmss}.zip"; - // HTTP: wwwroot/generated/ - if (_httpContextAccessor?.HttpContext is not null && _hostEnvironment is not null) - { - finalOutputDirectory = Path.Combine(_hostEnvironment.ContentRootPath, "wwwroot", "generated"); - } - // STDIO, 사용자가 경로를 지정하지 않은 경우: generated/ - else - { - finalOutputDirectory = Path.Combine(Directory.GetCurrentDirectory(), "generated"); - } + // Always use wwwroot/generated regardless of mode + finalOutputDirectory = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "generated"); - // 사용자가 outputDir을 지정한 경우 + // If user specifies outputDir, use that instead if (!string.IsNullOrWhiteSpace(outputDir)) { finalOutputDirectory = outputDir; From a9a2b8b64c859e0af2137fb1c2e3e04a1d485390 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Sat, 22 Nov 2025 21:56:32 +0900 Subject: [PATCH 33/61] Refactor Dockerfile and README to streamline volume mounting and command usage --- openapi-to-sdk/.vscode/mcp.stdio.container.json | 1 + openapi-to-sdk/README.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openapi-to-sdk/.vscode/mcp.stdio.container.json b/openapi-to-sdk/.vscode/mcp.stdio.container.json index 54444335..bffce64d 100644 --- a/openapi-to-sdk/.vscode/mcp.stdio.container.json +++ b/openapi-to-sdk/.vscode/mcp.stdio.container.json @@ -7,6 +7,7 @@ "run", "-i", "--rm", + "-v", "${pwd}/wwwroot/generated:/app/wwwroot/generated", "openapi-to-sdk:latest" ] } diff --git a/openapi-to-sdk/README.md b/openapi-to-sdk/README.md index 0df0e8b6..ec090e57 100644 --- a/openapi-to-sdk/README.md +++ b/openapi-to-sdk/README.md @@ -3,9 +3,9 @@ This is an MCP server that integrates with [Kiota](https://github.com/microsoft/kiota) to generate an SDK from OpenAPI documents. ```bash - docker run -i --rm -p 8080:8080 -v "$(pwd)/generated:/app/generated" openapi-to-sdk:latest + docker run -i --rm -p 8080:8080 openapi-to-sdk:latest ``` ```bash - docker run -i --rm -p 8080:8080 -v "$(pwd)/generated:/app/wwwroot/generated" openapi-to-sdk:latest --http + docker run -i --rm -p 8080:8080 openapi-to-sdk:latest --http ``` \ No newline at end of file From ef370699b273aac48bf1381d2a191944d76a479a Mon Sep 17 00:00:00 2001 From: x-or-b Date: Mon, 24 Nov 2025 23:48:51 +0900 Subject: [PATCH 34/61] Enhance OpenAPI specification handling by updating descriptions and adding support for local file paths in SDK generation methods --- .../Prompts/SdkGenerationPrompt.cs | 4 ++-- .../Tools/OpenApiToSdkTool.cs | 22 ++++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs index 04953ea2..1d09e7f1 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs @@ -31,7 +31,7 @@ public class SdkGenerationPrompt : ISdkGenerationPrompt [McpServerPrompt(Name = "generate_sdk", Title = "Generate SDK from OpenAPI Spec with Kiota Parsing")] [Description("Provides a structured prompt for parsing Kiota options and generating an SDK.")] public string GetSdkGenerationPrompt( - [Description("The Location of the OpenAPI description.")] string openApiDocUrl, + [Description("The URL or local file path of the OpenAPI description.")] string openApiDocUrl, [Description("The target language for the SDK.")] string language, [Description("The class name to use for the core client class. Defaults to ApiClient.")] string? className = null, [Description("The namespace to use for the core client class. Defaults to ApiSdk.")] string? namespaceName = null, @@ -40,7 +40,7 @@ public string GetSdkGenerationPrompt( return $""" Generate an SDK from the provided OpenAPI specification using Kiota. - OpenAPI Location: {openApiDocUrl} + OpenAPI Source: {openApiDocUrl} Language: {language} Class Name: {className ?? "Default (ApiClient)"} Namespace: {namespaceName ?? "Default (ApiSdk)"} diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs index 9c6a24db..ee2190d4 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs @@ -83,7 +83,7 @@ public async Task DownloadOpenApiSpecAsync( [McpServerTool(Name = "generate_sdk")] [Description("Generate an SDK from an OpenAPI specification using Kiota")] public async Task GenerateSdkAsync( - [Description("URL of the OpenAPI specification")] string openApiUrl, + [Description("URL or local file path of the OpenAPI specification")] string openApiUrl, [Description("Target language for the SDK (e.g., csharp, typescript)")] string language, [Description("Optional: The class name to use for the core client class. Defaults to ApiClient.")] string? className = null, [Description("Optional: The namespace to use for the core client class. Defaults to ApiSdk.")] string? namespaceName = null, @@ -96,10 +96,26 @@ public async Task GenerateSdkAsync( try { - var specContent = await _openApiService.DownloadOpenApiSpecAsync(openApiUrl); + string specContent; + // Check if it's a URL (http or https) + if (Uri.TryCreate(openApiUrl, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)) + { + specContent = await _openApiService.DownloadOpenApiSpecAsync(openApiUrl); + } + // Check if it's a local file that exists + else if (File.Exists(openApiUrl)) + { + specContent = await File.ReadAllTextAsync(openApiUrl); + } + else + { + result.ErrorMessage = $"The provided OpenAPI specification path or URL is not valid or accessible: {openApiUrl}"; + return result; + } + if (string.IsNullOrWhiteSpace(specContent)) { - result.ErrorMessage = "Failed to download or empty OpenAPI specification."; + result.ErrorMessage = "The OpenAPI specification content is empty."; return result; } From e699a86e9e9838972c1f1fe2c1b4f7686fa74a85 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Tue, 25 Nov 2025 22:47:27 +0900 Subject: [PATCH 35/61] Enhance Dockerfile and configuration files for OpenAPI to SDK integration, including Kiota CLI installation, updated volume paths, and improved Bicep module configurations. --- Dockerfile.openapi-to-sdk-azure | 10 +++++++- openapi-to-sdk/.vscode/mcp.http.remote.json | 2 +- .../.vscode/mcp.stdio.container.json | 3 ++- openapi-to-sdk/infra/abbreviations.json | 2 +- openapi-to-sdk/infra/main.parameters.json | 4 ++-- .../infra/modules/fetch-container-image.bicep | 2 +- openapi-to-sdk/infra/resources.bicep | 23 ++++++++++++++----- 7 files changed, 33 insertions(+), 13 deletions(-) diff --git a/Dockerfile.openapi-to-sdk-azure b/Dockerfile.openapi-to-sdk-azure index 6b290bd7..cd928c9f 100644 --- a/Dockerfile.openapi-to-sdk-azure +++ b/Dockerfile.openapi-to-sdk-azure @@ -2,6 +2,9 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build +# Install Kiota CLI +RUN dotnet tool install --global Microsoft.OpenApi.Kiota + COPY ./shared/McpSamples.Shared /source/shared/McpSamples.Shared COPY ./openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp /source/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp @@ -13,8 +16,13 @@ FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final WORKDIR /app +# Copy app and Kiota CLI COPY --from=build /app . +COPY --from=build /root/.dotnet/tools /opt/kiota-tools + +# Add Kiota to PATH by creating a symlink in a standard bin directory +RUN ln -s /opt/kiota-tools/kiota /usr/local/bin/kiota USER $APP_UID -ENTRYPOINT ["dotnet", "McpSamples.OpenApiToSdk.HybridApp.dll"] +ENTRYPOINT ["dotnet", "McpSamples.OpenApiToSdk.HybridApp.dll"] \ No newline at end of file diff --git a/openapi-to-sdk/.vscode/mcp.http.remote.json b/openapi-to-sdk/.vscode/mcp.http.remote.json index 4eb240be..368f825d 100644 --- a/openapi-to-sdk/.vscode/mcp.http.remote.json +++ b/openapi-to-sdk/.vscode/mcp.http.remote.json @@ -12,4 +12,4 @@ "url": "https://${input:acaapp-server-fqdn}/mcp" } } -} \ No newline at end of file +} diff --git a/openapi-to-sdk/.vscode/mcp.stdio.container.json b/openapi-to-sdk/.vscode/mcp.stdio.container.json index bffce64d..cab6f6ff 100644 --- a/openapi-to-sdk/.vscode/mcp.stdio.container.json +++ b/openapi-to-sdk/.vscode/mcp.stdio.container.json @@ -7,7 +7,8 @@ "run", "-i", "--rm", - "-v", "${pwd}/wwwroot/generated:/app/wwwroot/generated", + "-v", + "${workspaceFolder}/generated:/app/wwwroot/generated", "openapi-to-sdk:latest" ] } diff --git a/openapi-to-sdk/infra/abbreviations.json b/openapi-to-sdk/infra/abbreviations.json index 1533dee5..893310d3 100644 --- a/openapi-to-sdk/infra/abbreviations.json +++ b/openapi-to-sdk/infra/abbreviations.json @@ -133,4 +133,4 @@ "webSitesAppServiceEnvironment": "ase-", "webSitesFunctions": "func-", "webStaticSites": "stapp-" -} +} \ No newline at end of file diff --git a/openapi-to-sdk/infra/main.parameters.json b/openapi-to-sdk/infra/main.parameters.json index 05d30fab..9fafcc0d 100644 --- a/openapi-to-sdk/infra/main.parameters.json +++ b/openapi-to-sdk/infra/main.parameters.json @@ -9,10 +9,10 @@ "value": "${AZURE_LOCATION}" }, "mcpOpenApiToSdkExists": { - "value": "${SERVICE_MCP_OPENAPI_TO_SDK_RESOURCE_EXISTS=false}" + "value": "${SERVICE_OPENAPI_TO_SDK_RESOURCE_EXISTS=false}" }, "principalId": { "value": "${AZURE_PRINCIPAL_ID}" } } -} +} \ No newline at end of file diff --git a/openapi-to-sdk/infra/modules/fetch-container-image.bicep b/openapi-to-sdk/infra/modules/fetch-container-image.bicep index 78d1e7ee..d5108343 100644 --- a/openapi-to-sdk/infra/modules/fetch-container-image.bicep +++ b/openapi-to-sdk/infra/modules/fetch-container-image.bicep @@ -5,4 +5,4 @@ resource existingApp 'Microsoft.App/containerApps@2023-05-02-preview' existing = name: name } -output containers array = exists ? existingApp.properties.template.containers : [] +output containers array = exists ? existingApp.properties.template.containers : [] \ No newline at end of file diff --git a/openapi-to-sdk/infra/resources.bicep b/openapi-to-sdk/infra/resources.bicep index 55ab5575..9eb6f6c1 100644 --- a/openapi-to-sdk/infra/resources.bicep +++ b/openapi-to-sdk/infra/resources.bicep @@ -43,7 +43,7 @@ module containerRegistry 'br/public:avm/res/container-registry/registry:0.1.1' = } } -// Container apps environment +// Container apps environment with Storage Profile module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.4.5' = { name: 'container-apps-environment' params: { @@ -82,7 +82,7 @@ module mcpOpenApiToSdk 'br/public:avm/res/app/container-app:0.8.0' = { secrets: { secureList: [ ] - } + } containers: [ { image: mcpOpenApiToSdkFetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' @@ -91,9 +91,6 @@ module mcpOpenApiToSdk 'br/public:avm/res/app/container-app:0.8.0' = { cpu: json('0.5') memory: '1.0Gi' } - args: [ - '--http' - ] env: [ { name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' @@ -108,8 +105,12 @@ module mcpOpenApiToSdk 'br/public:avm/res/app/container-app:0.8.0' = { value: '8080' } ] + args: [ + '--http' + ] } ] + managedIdentities: { systemAssigned: false userAssignedResourceIds: [ @@ -123,6 +124,16 @@ module mcpOpenApiToSdk 'br/public:avm/res/app/container-app:0.8.0' = { } ] environmentResourceId: containerAppsEnvironment.outputs.resourceId + corsPolicy: { + allowedOrigins: [ + 'https://make.preview.powerapps.com' + 'https://make.powerapps.com' + 'https://make.preview.powerautomate.com' + 'https://make.powerautomate.com' + 'https://copilotstudio.preview.microsoft.com' + 'https://copilotstudio.microsoft.com' + ] + } location: location tags: union(tags, { 'azd-service-name': 'openapi-to-sdk' }) } @@ -131,4 +142,4 @@ module mcpOpenApiToSdk 'br/public:avm/res/app/container-app:0.8.0' = { output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer output AZURE_RESOURCE_MCP_OPENAPI_TO_SDK_ID string = mcpOpenApiToSdk.outputs.resourceId output AZURE_RESOURCE_MCP_OPENAPI_TO_SDK_NAME string = mcpOpenApiToSdk.outputs.name -output AZURE_RESOURCE_MCP_OPENAPI_TO_SDK_FQDN string = mcpOpenApiToSdk.outputs.fqdn +output AZURE_RESOURCE_MCP_OPENAPI_TO_SDK_FQDN string = mcpOpenApiToSdk.outputs.fqdn \ No newline at end of file From 91156dfdbcc51b4246de9f0bf41fa17ee401e994 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Tue, 25 Nov 2025 23:08:25 +0900 Subject: [PATCH 36/61] Refactor comments and formatting in Bicep files for clarity and consistency --- openapi-to-sdk/infra/resources.bicep | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi-to-sdk/infra/resources.bicep b/openapi-to-sdk/infra/resources.bicep index 9eb6f6c1..cc6d1e6f 100644 --- a/openapi-to-sdk/infra/resources.bicep +++ b/openapi-to-sdk/infra/resources.bicep @@ -43,7 +43,7 @@ module containerRegistry 'br/public:avm/res/container-registry/registry:0.1.1' = } } -// Container apps environment with Storage Profile +// Container apps environment module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.4.5' = { name: 'container-apps-environment' params: { @@ -82,7 +82,7 @@ module mcpOpenApiToSdk 'br/public:avm/res/app/container-app:0.8.0' = { secrets: { secureList: [ ] - } + } containers: [ { image: mcpOpenApiToSdkFetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' From 168c23fa76d281379e33c82686f47eb8af841a72 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Mon, 1 Dec 2025 20:13:16 +0900 Subject: [PATCH 37/61] Refactor Dockerfiles, enhance OpenApiService and SdkGenerationPrompt for improved SDK generation and error handling --- Dockerfile.openapi-to-sdk | 9 +- Dockerfile.openapi-to-sdk-azure | 5 +- .../.vscode/mcp.stdio.container.json | 2 +- .../Models/OpenApiToSdkResult.cs | 22 +- .../Program.cs | 6 +- .../Prompts/SdkGenerationPrompt.cs | 110 +++++--- .../Services/IOpenApiService.cs | 40 ++- .../Services/OpenApiService.cs | 256 ++++++++++++++---- .../Tools/OpenApiToSdkTool.cs | 206 +++----------- 9 files changed, 373 insertions(+), 283 deletions(-) diff --git a/Dockerfile.openapi-to-sdk b/Dockerfile.openapi-to-sdk index ba3294b1..81b1a3f8 100644 --- a/Dockerfile.openapi-to-sdk +++ b/Dockerfile.openapi-to-sdk @@ -27,12 +27,11 @@ COPY --from=build /root/.dotnet/tools /opt/kiota-tools RUN chmod -R 755 /opt/kiota-tools && \ ln -s /opt/kiota-tools/kiota /usr/local/bin/kiota -RUN chown $APP_UID /app - -USER $APP_UID +ENV PATH="/opt/kiota-tools:${PATH}" -RUN mkdir -p /app/wwwroot/generated +RUN mkdir -p /app/wwwroot/generated && \ + chown -R $APP_UID:$APP_UID /app/wwwroot -ENV PATH="/opt/kiota-tools:${PATH}" +USER $APP_UID ENTRYPOINT ["dotnet", "McpSamples.OpenApiToSdk.HybridApp.dll"] \ No newline at end of file diff --git a/Dockerfile.openapi-to-sdk-azure b/Dockerfile.openapi-to-sdk-azure index cd928c9f..3f953fb5 100644 --- a/Dockerfile.openapi-to-sdk-azure +++ b/Dockerfile.openapi-to-sdk-azure @@ -16,11 +16,12 @@ FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final WORKDIR /app -# Copy app and Kiota CLI COPY --from=build /app . COPY --from=build /root/.dotnet/tools /opt/kiota-tools -# Add Kiota to PATH by creating a symlink in a standard bin directory +RUN mkdir -p /app/wwwroot/generated && \ + chown -R $APP_UID:$APP_UID /app/wwwroot + RUN ln -s /opt/kiota-tools/kiota /usr/local/bin/kiota USER $APP_UID diff --git a/openapi-to-sdk/.vscode/mcp.stdio.container.json b/openapi-to-sdk/.vscode/mcp.stdio.container.json index cab6f6ff..9541a03f 100644 --- a/openapi-to-sdk/.vscode/mcp.stdio.container.json +++ b/openapi-to-sdk/.vscode/mcp.stdio.container.json @@ -7,7 +7,7 @@ "run", "-i", "--rm", - "-v", + "--volume", "${workspaceFolder}/generated:/app/wwwroot/generated", "openapi-to-sdk:latest" ] diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Models/OpenApiToSdkResult.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Models/OpenApiToSdkResult.cs index 157fae8f..ffbe9242 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Models/OpenApiToSdkResult.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Models/OpenApiToSdkResult.cs @@ -6,13 +6,29 @@ namespace McpSamples.OpenApiToSdk.HybridApp.Models; public class OpenApiToSdkResult { /// - /// Gets or sets the path to the generated ZIP file. - /// This will be a local file path. + /// Gets or sets the accessible path or URL to the generated ZIP file. + /// (e.g., "http://localhost:8080/generated/sdk.zip" or "C:\...\sdk.zip") /// public string? ZipPath { get; set; } + /// + /// Gets or sets the absolute internal file path on the server. + /// Useful for debugging or server-side logs. + /// + public string? ServerFilePath { get; set; } + + /// + /// Gets or sets a user-friendly message describing the outcome. + /// + public string? Message { get; set; } + /// /// Gets or sets the error message if the operation failed. /// public string? ErrorMessage { get; set; } -} + + /// + /// Gets a value indicating whether the operation was successful. + /// + public bool IsSuccess => string.IsNullOrEmpty(ErrorMessage); +} \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs index 52d48643..7bcdea4d 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs @@ -5,6 +5,7 @@ using McpSamples.Shared.Extensions; using McpSamples.Shared.OpenApi; +using Microsoft.AspNetCore.Builder; using Microsoft.OpenApi.Models; var useStreamableHttp = AppSettings.UseStreamableHttp(Environment.GetEnvironmentVariables(), args); @@ -34,10 +35,9 @@ IHost app = builder.BuildApp(useStreamableHttp); -if (useStreamableHttp == true) +if (app is WebApplication webApp) { - (app as WebApplication)!.MapOpenApi("/{documentName}.json"); - (app as WebApplication)!.UseStaticFiles(); + webApp.UseStaticFiles(); } await app.RunAsync(); \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs index 1d09e7f1..2e445caa 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs @@ -1,5 +1,4 @@ using System.ComponentModel; - using ModelContextProtocol.Server; namespace McpSamples.OpenApiToSdk.HybridApp.Prompts; @@ -9,49 +8,84 @@ namespace McpSamples.OpenApiToSdk.HybridApp.Prompts; /// public interface ISdkGenerationPrompt { - /// - /// Gets a prompt for generating an SDK from an OpenAPI specification, including parsing Kiota options. - /// - /// The location of the OpenAPI description. - /// The target language for the SDK. - /// Optional: The class name to use for the core client class. - /// Optional: The namespace to use for the core client class. - /// Additional user-provided options for Kiota. - /// A formatted prompt for SDK generation with parsing instructions. - string GetSdkGenerationPrompt(string openApiDocUrl, string language, string? className = null, string? namespaceName = null, string? additionalOptions = null); + /// + /// Gets a structured prompt that guides the LLM to generate an SDK. + /// + /// The target programming language for the SDK. + /// The OpenAPI source, which can be a URL, file path, or raw content. + /// The class name for the core client class (optional). + /// The namespace for the core client class (optional). + /// Additional options for Kiota (optional). + /// A formatted string representing the SDK generation prompt. + string GetSdkGenerationPrompt( + string language, + string openApiSource, + string? className = null, + string? namespaceName = null, + string? additionalOptions = null); } /// -/// This provides prompts for SDK generation from OpenAPI specs. +/// This represents the prompts entity for SDK generation. /// [McpServerPromptType] public class SdkGenerationPrompt : ISdkGenerationPrompt { - /// - [McpServerPrompt(Name = "generate_sdk", Title = "Generate SDK from OpenAPI Spec with Kiota Parsing")] - [Description("Provides a structured prompt for parsing Kiota options and generating an SDK.")] - public string GetSdkGenerationPrompt( - [Description("The URL or local file path of the OpenAPI description.")] string openApiDocUrl, - [Description("The target language for the SDK.")] string language, - [Description("The class name to use for the core client class. Defaults to ApiClient.")] string? className = null, - [Description("The namespace to use for the core client class. Defaults to ApiSdk.")] string? namespaceName = null, - [Description("Additional options for Kiota (e.g., '--include-path Paths').")] string? additionalOptions = null) - { - return $""" - Generate an SDK from the provided OpenAPI specification using Kiota. - - OpenAPI Source: {openApiDocUrl} - Language: {language} - Class Name: {className ?? "Default (ApiClient)"} - Namespace: {namespaceName ?? "Default (ApiSdk)"} - Additional Options: {additionalOptions ?? "None"} - - Instructions: - - Parse the options to valid Kiota command-line arguments. - - Use the 'generate_sdk' tool with the parsed options to process the spec. - - Validate the OpenAPI spec before generation. - - Return the ZIP file URI upon success. - - Handle errors gracefully and provide feedback. + /// + [McpServerPrompt(Name = "generate_sdk", Title = "Generate SDK from OpenAPI Spec")] + [Description("Returns a structured prompt that guides the LLM to generate an SDK using the 'generate_sdk' tool. It handles language normalization and input source resolution (URL vs File).")] + public string GetSdkGenerationPrompt( + [Description("The OpenAPI source. This can be a public URL, a local file path, OR the raw content (JSON/YAML).")] string openApiSource, + [Description("The target language (e.g. csharp, go, typescript).")] string language, + [Description("The class name to use for the core client class.")] string? className = null, + [Description("The namespace to use for the core client class.")] string? namespaceName = null, + [Description("Additional options for Kiota.")] string? additionalOptions = null) + { + return $""" + You are an expert SDK generator using Microsoft Kiota. + + 1. User Input Analysis + - OpenAPI Source: "{openApiSource}" + - Target Language: "{language}" + - Configuration: + - Class Name: {className ?? "Default (ApiClient)"} + - Namespace: {namespaceName ?? "Default (ApiSdk)"} + - Options: {additionalOptions ?? "None"} + + --- + 2. Execution Strategy (Follow Strictly) + + Step 1: Validate & Normalize Language + Match the input to a valid Kiota identifier: [ CSharp, Go, Java, PHP, Python, Ruby, Shell, Swift, TypeScript ]. + - If a match or alias is found (e.g., "ts" -> "TypeScript", "golang" -> "Go"), use the valid identifier. + - If NO match is found (e.g., "Rust", "C++", "asdf"), STOP immediately and ask the user to provide a supported language. + + Step 2: Resolve OpenAPI Source (CRITICAL) + The 'generate_sdk' tool accepts either a URL or Raw Content, but NOT a file path. + Analyze the [OpenAPI Source] provided above: + + - CASE A: It is a URL (starts with http/https) + - Action: Pass the URL string directly to the `specSource` argument. + + - CASE B: It looks like a File Path (e.g., "C:\specs\api.json", "./swagger.yaml") + - Action: You MUST first read the content of this file using your available tools (e.g., `filesystem` tool). + - Then, pass the file content (JSON/YAML text) to the `specSource` argument. + - If you cannot read the file, ask the user to paste the content directly. + + - CASE C: It is Raw JSON/YAML Content + - Action: Pass the content string directly to the `specSource` argument. + + Step 3: Call Tool + Call the `generate_sdk` tool with the prepared arguments: + - `language`: (The normalized identifier from Step 1) + - `specSource`: (The resolved URL or Content from Step 2) + - `className`: (As provided) + - `namespaceName`: (As provided) + - `additionalOptions`: (As provided) + + Step 4: Report Results + - If a download link is returned, display it clearly. + - If a local path is returned, provide the path. """; - } + } } \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/IOpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/IOpenApiService.cs index 27da9573..b9e3e504 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/IOpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/IOpenApiService.cs @@ -1,25 +1,43 @@ +using McpSamples.OpenApiToSdk.HybridApp.Models; + namespace McpSamples.OpenApiToSdk.HybridApp.Services; /// -/// This provides interfaces for OpenAPI service operations. +/// This provides interfaces for the OpenAPI service. /// public interface IOpenApiService { /// /// Downloads OpenAPI specification from a URL. /// - /// The URL to download from. - /// Cancellation token. - /// The downloaded content as a string. - Task DownloadOpenApiSpecAsync(string url, CancellationToken cancellationToken = default); + /// The URL of the OpenAPI specification. + /// A cancellation token. + /// The content of the OpenAPI specification as a string. + Task DownloadOpenApiSpecAsync(string openApiUrl, CancellationToken cancellationToken = default); + + /// + /// Generates a client SDK from an OpenAPI specification. + /// + /// The URL or raw content of the OpenAPI specification. + /// The target programming language for the SDK. + /// The name for the main client class (optional). + /// The namespace for the generated SDK (optional). + /// Additional options to pass to the Kiota CLI (optional). + /// An object containing the result of the SDK generation. + Task GenerateSdkAsync( + string specSource, + string language, + string? className = null, + string? namespaceName = null, + string? additionalOptions = null); /// - /// Runs Kiota CLI with the specified options. + /// Runs the Kiota command-line tool with the specified arguments. /// - /// Path to the OpenAPI spec file. - /// Target language for the SDK. - /// Output directory for the generated SDK. - /// Additional Kiota options. - /// Error message if failed, null if successful. + /// The path to the OpenAPI specification file or a URL. + /// The target programming language. + /// The directory where the generated SDK will be saved. + /// Additional options to pass to the Kiota CLI (optional). + /// An error message if the command fails; otherwise, null. Task RunKiotaAsync(string openApiSpecPath, string language, string outputDir, string? additionalOptions = null); } \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs index ff04a21c..0652dade 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs @@ -1,100 +1,256 @@ using System.Diagnostics; -using System.Text; +using System.IO.Compression; +using McpSamples.OpenApiToSdk.HybridApp.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; namespace McpSamples.OpenApiToSdk.HybridApp.Services; /// -/// This represents the service for OpenAPI operations. +/// This represents the service entity for OpenAPI operations. It handles downloading OpenAPI specifications, +/// generating SDKs using Kiota, and managing temporary files. /// -/// The instance. -/// The instance. -public class OpenApiService(HttpClient httpClient, ILogger logger) : IOpenApiService +/// An instance for making HTTP requests. +/// An instance for logging. +/// An to access the current HTTP context (optional). +public class OpenApiService( + HttpClient httpClient, + ILogger logger, + IHttpContextAccessor? httpContextAccessor = null) : IOpenApiService { /// - public async Task DownloadOpenApiSpecAsync(string url, CancellationToken cancellationToken = default) + public async Task GenerateSdkAsync( + string specSource, + string language, + string? className = null, + string? namespaceName = null, + string? additionalOptions = null) { - if (string.IsNullOrWhiteSpace(url)) + CleanupOldFiles(); + + var result = new OpenApiToSdkResult(); + string kiotaInputPath; + string? tempInputFile = null; + var tempGenDir = Path.Combine(Path.GetTempPath(), "kiota_gen_" + Guid.NewGuid()); + + try { - throw new ArgumentException("URL is required.", nameof(url)); + // Auto-detects if the source is a URL or raw content. + if (IsUrl(specSource)) + { + // If it's a URL, pass it directly to Kiota. + kiotaInputPath = specSource; + logger.LogInformation("Detected URL input: {Url}", specSource); + } + else + { + // If it's raw content, save it to a temporary file and pass the path. + tempInputFile = await CreateTempFileFromContentAsync(specSource); + kiotaInputPath = tempInputFile; + logger.LogInformation("Detected raw content input. Created temp file: {Path}", tempInputFile); + } + + var optionsList = new List(); + if (!string.IsNullOrWhiteSpace(className)) + { + optionsList.Add($"--class-name \"{className}\""); + } + if (!string.IsNullOrWhiteSpace(namespaceName)) + { + optionsList.Add($"--namespace-name \"{namespaceName}\""); + } + if (!string.IsNullOrWhiteSpace(additionalOptions)) + { + optionsList.Add(additionalOptions); + } + + var combinedOptions = string.Join(" ", optionsList); + + Directory.CreateDirectory(tempGenDir); + var kiotaError = await RunKiotaAsync(kiotaInputPath, language, tempGenDir, combinedOptions); + + if (!string.IsNullOrEmpty(kiotaError)) + { + result.ErrorMessage = kiotaError; + return result; + } + + var webRootPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot"); + var outputDir = Path.Combine(webRootPath, "generated"); + Directory.CreateDirectory(outputDir); + + var fileName = $"{language}-{DateTime.Now:yyyyMMddHHmmss}-{Guid.NewGuid().ToString()[..6]}.zip"; + var finalZipPath = Path.Combine(outputDir, fileName); + + ZipFile.CreateFromDirectory(tempGenDir, finalZipPath); + + result.ServerFilePath = finalZipPath; + var request = httpContextAccessor?.HttpContext?.Request; + + if (request != null) + { + var baseUrl = $"{request.Scheme}://{request.Host}"; + result.ZipPath = $"{baseUrl}/generated/{fileName}"; + result.Message = $"SDK generation successful. Download link: {result.ZipPath}"; + } + else + { + var fileUri = "file:///" + finalZipPath.Replace("\\", "/").TrimStart('/'); + result.ZipPath = fileUri; + result.Message = $"SDK generation successful! File Location: {fileUri}"; + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error during SDK generation workflow."); + result.ErrorMessage = ex.Message; + } + finally + { + CleanupTempResources(tempInputFile, tempGenDir); } - logger.LogInformation("Downloading OpenAPI spec from {Url}", url); - var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); + return result; + } + + /// + /// Determines if the given string is an absolute URL. + /// + /// The string to check. + /// true if the input is a valid HTTP or HTTPS URL; otherwise, false. + private bool IsUrl(string input) + { + return Uri.TryCreate(input, UriKind.Absolute, out var uriResult) + && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); + } - var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - logger.LogInformation("Downloaded OpenAPI spec from {Url} (Length={Length})", url, content.Length); + /// + /// Deletes old generated SDK files from the output directory to save space. + /// + private void CleanupOldFiles() + { + try + { + var webRootPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot"); + var outputDir = Path.Combine(webRootPath, "generated"); - return content; + if (Directory.Exists(outputDir)) + { + var files = Directory.GetFiles(outputDir); + foreach (var file in files) + { + if (DateTime.Now - File.GetCreationTime(file) > TimeSpan.FromHours(1)) + { + try { File.Delete(file); } + catch + { + // Ignored + } + } + } + } + } + catch (Exception ex) + { + logger.LogWarning("Failed to clean up old generated files: {Message}", ex.Message); + } } - /// - public async Task RunKiotaAsync(string openApiSpecPath, string language, string outputDir, string? additionalOptions = null) + /// + /// Cleans up temporary files and directories created during SDK generation. + /// + /// The path to the temporary input file to delete. + /// The path to the temporary generation directory to delete. + private void CleanupTempResources(string? tempFile, string tempDir) { - // Map Kiota command options - var arguments = new StringBuilder(); - arguments.Append($"generate"); - arguments.Append($" --openapi \"{openApiSpecPath}\" --language {language} --output \"{outputDir}\""); - if (!string.IsNullOrWhiteSpace(additionalOptions)) + if (tempFile != null && File.Exists(tempFile)) { - arguments.Append($" {additionalOptions}"); + try { File.Delete(tempFile); } + catch (Exception ex) { logger.LogWarning("Failed to delete temp file: {Message}", ex.Message); } } + if (Directory.Exists(tempDir)) + { + try { Directory.Delete(tempDir, true); } + catch (Exception ex) { logger.LogWarning("Failed to delete temp dir: {Message}", ex.Message); } + } + } - var processStartInfo = new ProcessStartInfo + /// + /// Creates a temporary JSON file from a string content. + /// + /// The string content to write to the file. + /// The path to the newly created temporary file. + private async Task CreateTempFileFromContentAsync(string content) + { + var tempPath = Path.GetTempFileName(); + var jsonPath = Path.ChangeExtension(tempPath, ".json"); + if (File.Exists(tempPath)) { - FileName = "kiota", - Arguments = arguments.ToString(), - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; + File.Move(tempPath, jsonPath, true); + } + await File.WriteAllTextAsync(jsonPath, content); + return jsonPath; + } + /// + public async Task DownloadOpenApiSpecAsync(string openApiUrl, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(openApiUrl)) + { + throw new ArgumentException("URL is required.", nameof(openApiUrl)); + } + var response = await httpClient.GetAsync(openApiUrl, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + } + + /// + public async Task RunKiotaAsync(string openApiSpecPath, string language, string outputDir, string? additionalOptions = null) + { try { - using var process = Process.Start(processStartInfo); - if (process is null) + var arguments = $"generate -l {language} -d \"{openApiSpecPath}\" -o \"{outputDir}\" {additionalOptions}"; + + var startInfo = new ProcessStartInfo { - return "Failed to start Kiota process."; - } + FileName = "kiota", + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + logger.LogInformation("Running Kiota: kiota {Arguments}", arguments); + + using var process = new Process { StartInfo = startInfo }; + process.Start(); - // Execute with timeout (5 minutes) var timeout = TimeSpan.FromMinutes(5); - var outputTask = process.StandardOutput.ReadToEndAsync(); - var errorTask = process.StandardError.ReadToEndAsync(); var exitTask = process.WaitForExitAsync(); if (await Task.WhenAny(exitTask, Task.Delay(timeout)).ConfigureAwait(false) == exitTask) { - await Task.WhenAll(outputTask, errorTask).ConfigureAwait(false); - if (process.ExitCode != 0) { - logger.LogError("Kiota execution failed (ExitCode={ExitCode}): {Error}", process.ExitCode, await errorTask); - return $"Kiota error: {await errorTask}"; + var error = await process.StandardError.ReadToEndAsync(); + return $"Kiota error: {error}"; } - - logger.LogInformation("Kiota execution succeeded: {Output}", await outputTask); return null; } else { - try - { - process.Kill(entireProcessTree: true); - } + try { process.Kill(entireProcessTree: true); } catch { - // Best effort cleanup + // Ignored } - return "Kiota execution timed out."; } } catch (Exception ex) { - logger.LogError(ex, "Exception during Kiota execution"); return $"Kiota exception: {ex.Message}"; } } diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs index ee2190d4..c3719517 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs @@ -1,196 +1,62 @@ -using System; using System.ComponentModel; -using System.IO; -using System.IO.Compression; -using System.Text; -using System.Threading.Tasks; - using McpSamples.OpenApiToSdk.HybridApp.Models; using McpSamples.OpenApiToSdk.HybridApp.Services; - -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; - using ModelContextProtocol.Server; namespace McpSamples.OpenApiToSdk.HybridApp.Tools; /// -/// This provides interfaces for the OpenAPI to SDK tool. +/// Represents the tool for generating an SDK from an OpenAPI specification. /// -public interface IOpenApiToSdkTool +/// The service for handling OpenAPI operations. +/// The logger for this tool. +[McpServerToolType] +public class OpenApiToSdkTool(IOpenApiService openApiService, ILogger logger) { /// - /// Downloads OpenAPI specification from a URL. - /// - /// The URL of the OpenAPI specification. - /// A containing the downloaded content or an error message. - Task DownloadOpenApiSpecAsync(string openApiUrl); - - /// - /// Generates an SDK from an OpenAPI specification using Kiota. + /// Generates a client SDK from an OpenAPI specification provided as a URL or raw content. /// - /// The URL of the OpenAPI specification. - /// The target language for the SDK (e.g., "csharp", "typescript"). - /// Optional: The class name to use for the core client class. - /// Optional: The namespace to use for the core client class. - /// Additional Kiota CLI options. - /// Optional: The directory where the generated SDK ZIP file will be saved. If not provided, a default 'generated' folder will be used. - /// An containing the path to the generated SDK ZIP file or an error message. - Task GenerateSdkAsync(string openApiUrl, string language, string? className = null, string? namespaceName = null, string? additionalOptions = null, string? outputDir = null); -} - -/// -/// This represents the tool entity for OpenAPI to SDK operations. -/// -[McpServerToolType] -public class OpenApiToSdkTool : IOpenApiToSdkTool -{ - private readonly IOpenApiService _openApiService; - private readonly ILogger _logger; - private readonly IHostEnvironment? _hostEnvironment; - private readonly IHttpContextAccessor? _httpContextAccessor; - - public OpenApiToSdkTool(IOpenApiService openApiService, ILogger logger, IHostEnvironment? hostEnvironment = null, IHttpContextAccessor? httpContextAccessor = null) - { - _openApiService = openApiService ?? throw new ArgumentNullException(nameof(openApiService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _hostEnvironment = hostEnvironment; - _httpContextAccessor = httpContextAccessor; - } - - /// - [McpServerTool(Name = "download_openapi_spec")] - [Description("Download OpenAPI specification from a URL")] - public async Task DownloadOpenApiSpecAsync( - [Description("URL of the OpenAPI specification")] string openApiUrl) + /// The URL or raw text content of the OpenAPI specification (JSON/YAML). + /// The target language for the SDK (e.g., CSharp, Java, Python). + /// The name for the client class (optional). + /// The namespace for the generated client code (optional). + /// Additional command-line options to pass to the Kiota tool (optional). + /// An containing the result of the generation process. + [McpServerTool(Name = "generate_sdk", Title = "Generate SDK from OpenAPI")] + [Description("Generates a client SDK. Accepts either a URL or raw OpenAPI Content (JSON/YAML).")] + public async Task GenerateSdkAsync( + [Description("The OpenAPI source. Provide a URL (http://...) OR the raw content text (JSON/YAML).")] string specSource, + [Description("The target language (e.g., CSharp, Java, Python).")] string language, + [Description("The class name for the client (optional).")] string? className = null, + [Description("The namespace for the client (optional).")] string? namespaceName = null, + [Description("Additional Kiota CLI options (optional).")] string? additionalOptions = null) { - var result = new DownloadResult(); - try + // Validate input + if (string.IsNullOrWhiteSpace(specSource)) { - result.Content = await _openApiService.DownloadOpenApiSpecAsync(openApiUrl); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error downloading OpenAPI spec from {Url}", openApiUrl); - result.ErrorMessage = ex.Message; + return new OpenApiToSdkResult + { + ErrorMessage = "The 'specSource' parameter is required. It must be a URL or OpenAPI content." + }; } - return result; - } - /// - [McpServerTool(Name = "generate_sdk")] - [Description("Generate an SDK from an OpenAPI specification using Kiota")] - public async Task GenerateSdkAsync( - [Description("URL or local file path of the OpenAPI specification")] string openApiUrl, - [Description("Target language for the SDK (e.g., csharp, typescript)")] string language, - [Description("Optional: The class name to use for the core client class. Defaults to ApiClient.")] string? className = null, - [Description("Optional: The namespace to use for the core client class. Defaults to ApiSdk.")] string? namespaceName = null, - [Description("Optional extra Kiota options (e.g., --include-path Paths)")] string? additionalOptions = null, - [Description("Optional: The directory where the generated SDK ZIP file will be saved. If not provided, a default 'generated' folder will be used.")] string? outputDir = null) - { - var result = new OpenApiToSdkResult(); - var tempSpecPath = string.Empty; - var sdkOutputDir = string.Empty; + logger.LogInformation("Generating SDK for language: {Language}", language); try { - string specContent; - // Check if it's a URL (http or https) - if (Uri.TryCreate(openApiUrl, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)) - { - specContent = await _openApiService.DownloadOpenApiSpecAsync(openApiUrl); - } - // Check if it's a local file that exists - else if (File.Exists(openApiUrl)) - { - specContent = await File.ReadAllTextAsync(openApiUrl); - } - else - { - result.ErrorMessage = $"The provided OpenAPI specification path or URL is not valid or accessible: {openApiUrl}"; - return result; - } - - if (string.IsNullOrWhiteSpace(specContent)) - { - result.ErrorMessage = "The OpenAPI specification content is empty."; - return result; - } - - var tempDir = Path.GetTempPath(); - tempSpecPath = Path.Combine(tempDir, $"openapi-spec-{Guid.NewGuid():N}.json"); - await File.WriteAllTextAsync(tempSpecPath, specContent); - - sdkOutputDir = Path.Combine(tempDir, $"sdk-output-{Guid.NewGuid():N}"); - Directory.CreateDirectory(sdkOutputDir); - - var kiotaOptions = new StringBuilder(additionalOptions ?? ""); - if (!string.IsNullOrWhiteSpace(className)) - { - kiotaOptions.Append($" --class-name {className}"); - } - if (!string.IsNullOrWhiteSpace(namespaceName)) - { - kiotaOptions.Append($" --namespace-name {namespaceName}"); - } - - var error = await _openApiService.RunKiotaAsync(tempSpecPath, language, sdkOutputDir, kiotaOptions.ToString().Trim()); - if (!string.IsNullOrEmpty(error)) - { - result.ErrorMessage = $"SDK generation failed: {error}"; - return result; - } - - string finalOutputDirectory; - string zipFileName = $"{language}-{DateTime.Now:yyyyMMddHHmmss}.zip"; - - // Always use wwwroot/generated regardless of mode - finalOutputDirectory = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "generated"); - - // If user specifies outputDir, use that instead - if (!string.IsNullOrWhiteSpace(outputDir)) - { - finalOutputDirectory = outputDir; - } - - Directory.CreateDirectory(finalOutputDirectory); - string finalZipPath = Path.Combine(finalOutputDirectory, zipFileName); - - // Compress generated SDK to ZIP - ZipFile.CreateFromDirectory(sdkOutputDir, finalZipPath); - - // HTTP 모드에서는 URI를, 그렇지 않으면 로컬 경로를 반환 - if (_httpContextAccessor?.HttpContext is not null && _hostEnvironment is not null) - { - var request = _httpContextAccessor.HttpContext.Request; - var baseUrl = $"{request.Scheme}://{request.Host}"; - result.ZipPath = $"{baseUrl}/generated/{zipFileName}"; - } - else - { - result.ZipPath = finalZipPath; - } + // Call the service to perform the generation + return await openApiService.GenerateSdkAsync( + specSource, + language, + className, + namespaceName, + additionalOptions); } catch (Exception ex) { - _logger.LogError(ex, "An exception occurred during SDK generation."); - result.ErrorMessage = ex.Message; - } - finally - { - // Cleanup temporary files and directories - if (!string.IsNullOrEmpty(tempSpecPath) && File.Exists(tempSpecPath)) - { - File.Delete(tempSpecPath); - } - if (!string.IsNullOrEmpty(sdkOutputDir) && Directory.Exists(sdkOutputDir)) - { - Directory.Delete(sdkOutputDir, true); - } + logger.LogError(ex, "Failed to execute generate_sdk tool."); + return new OpenApiToSdkResult { ErrorMessage = $"Tool execution error: {ex.Message}" }; } - - return result; } } \ No newline at end of file From a03c6ee9add2429ea598ccf833a0bc74c0293008 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Mon, 1 Dec 2025 20:25:52 +0900 Subject: [PATCH 38/61] Update .gitignore to include test directory and remove DownloadResult model class --- openapi-to-sdk/.gitignore | 3 ++- .../Models/DownloadResult.cs | 17 ----------------- 2 files changed, 2 insertions(+), 18 deletions(-) delete mode 100644 openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Models/DownloadResult.cs diff --git a/openapi-to-sdk/.gitignore b/openapi-to-sdk/.gitignore index b9006ccf..78b461c0 100644 --- a/openapi-to-sdk/.gitignore +++ b/openapi-to-sdk/.gitignore @@ -1,4 +1,5 @@ !.vscode/mcp.json .azure -image.png \ No newline at end of file +image.png +test/ \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Models/DownloadResult.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Models/DownloadResult.cs deleted file mode 100644 index 99296045..00000000 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Models/DownloadResult.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace McpSamples.OpenApiToSdk.HybridApp.Models; - -/// -/// Represents the result of a download operation. -/// -public class DownloadResult -{ - /// - /// Gets or sets the downloaded content. - /// - public string? Content { get; set; } - - /// - /// Gets or sets the error message if the operation failed. - /// - public string? ErrorMessage { get; set; } -} From 53a3ac9a7834720ed9670c753ab297db47ade7f2 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Mon, 1 Dec 2025 20:50:06 +0900 Subject: [PATCH 39/61] Update README.md to reflect OpenAPI to SDK Generator details and installation instructions --- openapi-to-sdk/README.md | 276 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 269 insertions(+), 7 deletions(-) diff --git a/openapi-to-sdk/README.md b/openapi-to-sdk/README.md index ec090e57..74e66f07 100644 --- a/openapi-to-sdk/README.md +++ b/openapi-to-sdk/README.md @@ -1,11 +1,273 @@ -# MCP Server: Awesome Copilot +# MCP Server: OpenAPI to SDK Generator -This is an MCP server that integrates with [Kiota](https://github.com/microsoft/kiota) to generate an SDK from OpenAPI documents. +This is an MCP server that generates client SDKs from OpenAPI specifications using Microsoft Kiota. -```bash +## 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)]() [![Install in Visual Studio](https://img.shields.io/badge/Visual_Studio-Install-C16FDE?logo=visualstudio&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 + +OpenAPI to SDK MCP server includes: + +| Building Block | Name | Description | Usage | +|----------------|----------------|---------------------------------------------------------------------------------------------------------|-----------------------| +| Tools | `generate_sdk` | Generates a client SDK from an OpenAPI specification (URL or raw content) and returns a download link. | `#generate_sdk` | +| Prompts | `generate_sdk` | A structured prompt that guides the LLM to generate an SDK, handling language normalization and inputs. | `/mcp.openapi-to-sdk.generate_sdk` | + +## 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) + +### 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 + +##### Prerequisites +- [Kiota](https://learn.microsoft.com/en-us/openapi/kiota/install?tabs=bash) + +1. Run the MCP server app. + + ```bash + cd $REPOSITORY_ROOT/openapi-to-sdk + dotnet run --project ./src/McpSamples.OpenApiToSdk.HybridApp + ``` + + > Make sure take note the absolute directory path of the `McpSamples.OpenApiToSdk.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:5220`. + + Example running in HTTP mode: + + ```bash + dotnet run --project ./src/McpSamples.OpenApiToSdk.HybridApp -- --http + +#### In a container + +1. Build the MCP server app as a container image. + + ```bash + cd $REPOSITORY_ROOT + docker build -f Dockerfile.openapi-to-sdk -t openapi-to-sdk:latest . + ``` + +1. Run the MCP server app in a container. + + ```bash docker run -i --rm -p 8080:8080 openapi-to-sdk:latest -``` + ``` + + Alternatively, use the container image from the container registry. + + ```bash + docker run -i --rm -p 8080:8080 ghcr.io/microsoft/mcp-dotnet-samples/openapi-to-sdk:latest + ``` + + **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 this parameter, you can run the MCP server like: + + ```bash + # use local container image + docker run -i --rm -p 8080:8080 openapi-to-sdk:latest --http + ``` + + ```bash + # use container image from the container registry + docker run -i --rm -p 8080:8080 ghcr.io/microsoft/mcp-dotnet-samples/openapi-to-sdk:latest --http + ``` + +#### On Azure + +1. Navigate to the directory. + + ```bash + cd $REPOSITORY_ROOT/openapi-to-sdk + ``` + +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_OPENAPI_TO_SDK_FQDN + ``` + +### 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/openapi-to-sdk/.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/openapi-to-sdk/.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/openapi-to-sdk/.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/openapi-to-sdk/.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/openapi-to-sdk/.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/openapi-to-sdk/.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/openapi-to-sdk/.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/openapi-to-sdk/.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/openapi-to-sdk/.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/openapi-to-sdk/.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 `openapi-to-sdk` then click `Start Server`. +1. When prompted, enter one of the following values: + - The absolute directory path of the `McpSamples.OpenApiToSdk.HybridApp` project + - The FQDN of Azure Container Apps. +1. Use a prompt by typing `/mcp.openapi-to-sdk.generate_sdk` and enter keywords to search. You'll get a prompt like: + + ```text + You are an expert SDK generator using Microsoft Kiota. + + 1. User Input Analysis + - OpenAPI Source: "{openApiSource}" + - Target Language: "{language}" + - Configuration: + - Class Name: {className ?? "Default (ApiClient)"} + - Namespace: {namespaceName ?? "Default (ApiSdk)"} + - Options: {additionalOptions ?? "None"} + + --- + 2. Execution Strategy (Follow Strictly) + + Step 1: Validate & Normalize Language + Match the input to a valid Kiota identifier: [ CSharp, Go, Java, PHP, Python, Ruby, Shell, Swift, TypeScript ]. + - If a match or alias is found (e.g., "ts" -> "TypeScript", "golang" -> "Go"), use the valid identifier. + - If NO match is found (e.g., "Rust", "C++", "asdf"), STOP immediately and ask the user to provide a supported language. + + Step 2: Resolve OpenAPI Source (CRITICAL) + The 'generate_sdk' tool accepts either a URL or Raw Content, but NOT a file path. + Analyze the [OpenAPI Source] provided above: + + - CASE A: It is a URL (starts with http/https) + - Action: Pass the URL string directly to the `specSource` argument. + + - CASE B: It looks like a File Path (e.g., "C:\specs\api.json", "./swagger.yaml") + - Action: You MUST first read the content of this file using your available tools (e.g., `filesystem` tool). + - Then, pass the file content (JSON/YAML text) to the `specSource` argument. + - If you cannot read the file, ask the user to paste the content directly. + + - CASE C: It is Raw JSON/YAML Content + - Action: Pass the content string directly to the `specSource` argument. + + Step 3: Call Tool + Call the `generate_sdk` tool with the prepared arguments: + - `language`: (The normalized identifier from Step 1) + - `specSource`: (The resolved URL or Content from Step 2) + - `className`: (As provided) + - `namespaceName`: (As provided) + - `additionalOptions`: (As provided) + + Step 4: Report Results + - If a download link is returned, display it clearly. + - If a local path is returned, provide the path. + ``` -```bash - docker run -i --rm -p 8080:8080 openapi-to-sdk:latest --http -``` \ No newline at end of file +1. Confirm the result. \ No newline at end of file From 39eb9120055c633bcec5066c8d10d1b937f22d36 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Mon, 1 Dec 2025 20:53:27 +0900 Subject: [PATCH 40/61] Update README.md to include Kiota as a prerequisite for the OpenAPI to SDK Generator --- openapi-to-sdk/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openapi-to-sdk/README.md b/openapi-to-sdk/README.md index 74e66f07..b4320661 100644 --- a/openapi-to-sdk/README.md +++ b/openapi-to-sdk/README.md @@ -14,6 +14,7 @@ This is an MCP server that generates client SDKs from OpenAPI specifications usi - [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/) +- [Kiota](https://learn.microsoft.com/en-us/openapi/kiota/install?tabs=bash) ## What's Included @@ -51,9 +52,6 @@ OpenAPI to SDK MCP server includes: #### On a local machine -##### Prerequisites -- [Kiota](https://learn.microsoft.com/en-us/openapi/kiota/install?tabs=bash) - 1. Run the MCP server app. ```bash From f800f051af58b438873f9e0a40debae0721a4085 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Wed, 3 Dec 2025 20:41:05 +0900 Subject: [PATCH 41/61] Refactor OpenAPI to SDK generation logic and enhance configuration settings - Updated mcp.json to streamline server configuration. - Improved OpenApiToSdkAppSettings with additional runtime properties. - Refined OpenApiToSdkResult to better handle SDK generation outcomes. - Enhanced Program.cs for improved runtime environment detection and settings initialization. - Updated SdkGenerationPrompt to clarify prompt generation for SDKs. - Simplified IOpenApiService and OpenApiService interfaces for better clarity and functionality. - Refactored OpenApiToSdkTool to streamline SDK generation process. --- .vscode/mcp.json | 16 +- .../Configurations/OpenApiToSdkAppSettings.cs | 40 ++- .../Models/OpenApiToSdkResult.cs | 29 +- .../Program.cs | 125 ++++++-- .../Prompts/SdkGenerationPrompt.cs | 105 +++---- .../Services/IOpenApiService.cs | 41 +-- .../Services/OpenApiService.cs | 294 +++++------------- .../Tools/OpenApiToSdkTool.cs | 99 +++--- 8 files changed, 342 insertions(+), 407 deletions(-) diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 61c9bfd9..eee37b48 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,20 +1,8 @@ { - "inputs": [ - { - "type": "promptString", - "id": "consoleapp-project-path", - "description": "The absolute path to the console app project Directory" - } - ], "servers": { "openapi-to-sdk": { - "type": "stdio", - "command": "dotnet", - "args": [ - "run", - "--project", - "${input:consoleapp-project-path}" - ] + "type": "http", + "url": "http://localhost:5222/mcp" } } } \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Configurations/OpenApiToSdkAppSettings.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Configurations/OpenApiToSdkAppSettings.cs index 067d2c2a..73966819 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Configurations/OpenApiToSdkAppSettings.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Configurations/OpenApiToSdkAppSettings.cs @@ -1,11 +1,11 @@ using McpSamples.Shared.Configurations; - using Microsoft.OpenApi.Models; namespace McpSamples.OpenApiToSdk.HybridApp.Configurations; /// -/// This represents the application settings for openapi-to-sdk app. +/// Represents the application settings for the OpenApiToSdk app. +/// Inherits from Shared AppSettings to maintain consistency. /// public class OpenApiToSdkAppSettings : AppSettings { @@ -14,6 +14,40 @@ public class OpenApiToSdkAppSettings : AppSettings { Title = "MCP OpenAPI to SDK", Version = "1.0.0", - Description = "A simple MCP server for integrating Kiota to generate an SDK from OpenAPI documents." + Description = "An MCP server that generates client SDKs from OpenAPI specifications using Kiota." }; + + // -------------------------------------------------------- + // Runtime Configurations (Program.cs에서 계산 후 할당될 값들) + // -------------------------------------------------------- + + /// + /// The root path for the workspace (shared volume or local folder). + /// + public string WorkspacePath { get; set; } = string.Empty; + + /// + /// The path where generated SDKs (zip files) will be stored. + /// + public string GeneratedPath { get; set; } = string.Empty; + + /// + /// The path where spec files are stored (or mounted). + /// + public string SpecsPath { get; set; } = string.Empty; + + /// + /// Indicates if the app is running in HTTP mode (vs Stdio). + /// + public bool IsHttpMode { get; set; } + + /// + /// Indicates if the app is running inside a Docker container. + /// + public bool IsContainer { get; set; } + + /// + /// Indicates if the app is running in Azure Container Apps. + /// + public bool IsAzure { get; set; } } \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Models/OpenApiToSdkResult.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Models/OpenApiToSdkResult.cs index ffbe9242..e69e26a9 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Models/OpenApiToSdkResult.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Models/OpenApiToSdkResult.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace McpSamples.OpenApiToSdk.HybridApp.Models; /// @@ -6,29 +8,20 @@ namespace McpSamples.OpenApiToSdk.HybridApp.Models; public class OpenApiToSdkResult { /// - /// Gets or sets the accessible path or URL to the generated ZIP file. - /// (e.g., "http://localhost:8080/generated/sdk.zip" or "C:\...\sdk.zip") - /// - public string? ZipPath { get; set; } - - /// - /// Gets or sets the absolute internal file path on the server. - /// Useful for debugging or server-side logs. - /// - public string? ServerFilePath { get; set; } - - /// - /// Gets or sets a user-friendly message describing the outcome. + /// Gets or sets a value indicating whether the generation was successful. /// - public string? Message { get; set; } + [JsonPropertyName("isSuccess")] + public bool IsSuccess { get; set; } /// - /// Gets or sets the error message if the operation failed. + /// Gets or sets the message or output path. /// - public string? ErrorMessage { get; set; } + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; /// - /// Gets a value indicating whether the operation was successful. + /// Gets or sets the download URL (if applicable). /// - public bool IsSuccess => string.IsNullOrEmpty(ErrorMessage); + [JsonPropertyName("downloadUrl")] + public string? DownloadUrl { get; set; } } \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs index 7bcdea4d..8eac7471 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs @@ -1,43 +1,130 @@ +using System.Text.Json; +using Microsoft.Extensions.FileProviders; using McpSamples.OpenApiToSdk.HybridApp.Configurations; +using McpSamples.OpenApiToSdk.HybridApp.Prompts; using McpSamples.OpenApiToSdk.HybridApp.Services; -using McpSamples.OpenApiToSdk.HybridApp.Tools; using McpSamples.Shared.Configurations; using McpSamples.Shared.Extensions; -using McpSamples.Shared.OpenApi; - -using Microsoft.AspNetCore.Builder; -using Microsoft.OpenApi.Models; +// 1. 실행 모드 감지 (Shared 기능 사용) var useStreamableHttp = AppSettings.UseStreamableHttp(Environment.GetEnvironmentVariables(), args); +// 2. 호스트 빌더 생성 IHostApplicationBuilder builder = useStreamableHttp ? WebApplication.CreateBuilder(args) : Host.CreateApplicationBuilder(args); +// 3. 설정(AppSettings) 등록 (Shared 기능 사용) +// appsettings.json 로드 및 객체 바인딩 처리 builder.Services.AddAppSettings(builder.Configuration, args); -builder.Services.AddHttpClient(); +// 4. 서비스 등록 +var options = new JsonSerializerOptions +{ + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + AllowTrailingCommas = true, + PropertyNameCaseInsensitive = true +}; +builder.Services.AddSingleton(options); + +// 핵심 비즈니스 로직 서비스 및 프롬프트 서비스 등록 +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// 5. 앱 빌드 (Shared 기능 사용) +// 이 단계에서 [McpServerToolType], [McpServerPromptType] 어트리뷰트가 있는 클래스를 자동으로 스캔하여 등록합니다. +IHost app = builder.BuildApp(useStreamableHttp); + +// -------------------------------------------------------------------------- +// [Runtime Configuration] 실행 환경(Local/Docker/Azure)에 따른 경로 설정 +// -------------------------------------------------------------------------- +var appSettings = app.Services.GetRequiredService(); +InitializeRuntimeSettings(appSettings, useStreamableHttp); -if (useStreamableHttp == true) +// -------------------------------------------------------------------------- +// [HTTP Mode Only] 다운로드를 위한 정적 파일 서빙 설정 +// -------------------------------------------------------------------------- +if (useStreamableHttp) { - builder.Services.AddHttpContextAccessor(); - builder.Services.AddOpenApi("swagger", o => + var webApp = (app as WebApplication)!; + + // 저장 경로가 없으면 생성 + if (!Directory.Exists(appSettings.GeneratedPath)) { - o.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0; - o.AddDocumentTransformer>(); - }); - builder.Services.AddOpenApi("openapi", o => + Directory.CreateDirectory(appSettings.GeneratedPath); + } + + // '/download' 경로를 물리적 폴더에 매핑 + webApp.UseStaticFiles(new StaticFileOptions { - o.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0; - o.AddDocumentTransformer>(); + FileProvider = new PhysicalFileProvider(appSettings.GeneratedPath), + RequestPath = "/download", + ServeUnknownFileTypes = true }); } -IHost app = builder.BuildApp(useStreamableHttp); +// 6. 앱 실행 +await app.RunAsync(); + -if (app is WebApplication webApp) +// -------------------------------------------------------------------------- +// Helper: 런타임 환경 변수 감지 및 경로 주입 +// -------------------------------------------------------------------------- +void InitializeRuntimeSettings(OpenApiToSdkAppSettings settings, bool isHttp) { - webApp.UseStaticFiles(); + // Docker/Azure 환경 변수 확인 + bool isContainer = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true"; + string? azureAppName = Environment.GetEnvironmentVariable("CONTAINER_APP_NAME"); + bool isAzure = !string.IsNullOrEmpty(azureAppName); + + string baseDirectory; + + if (isContainer) + { + // 컨테이너 환경: 항상 /app 사용 + baseDirectory = "/app"; + } + else + { + // 로컬 환경: 'openapi-to-sdk' 루트 폴더 찾기 + // 1. 현재 실행 위치(CurrentDirectory)에서 시작 + // 2. 상위로 이동하며 'Dockerfile.openapi-to-sdk' 파일이 있는 곳을 찾음 + baseDirectory = TryFindProjectRoot(Directory.GetCurrentDirectory()) ?? Directory.GetCurrentDirectory(); + baseDirectory = Path.Combine(baseDirectory, "openapi-to-sdk"); + + // (디버깅용 로그: 실행 시 콘솔에 출력됨) + Console.WriteLine($"[Init] Local Base Directory resolved to: {baseDirectory}"); + } + + string workspacePath = Path.Combine(baseDirectory, "workspace"); + + // 설정 객체에 값 주입 + settings.WorkspacePath = workspacePath; + settings.GeneratedPath = Path.Combine(workspacePath, "generated"); + settings.SpecsPath = Path.Combine(workspacePath, "specs"); + settings.IsHttpMode = isHttp; + settings.IsContainer = isContainer; + settings.IsAzure = isAzure; + + // 필수 폴더 생성 + if (!Directory.Exists(settings.WorkspacePath)) Directory.CreateDirectory(settings.WorkspacePath); + if (!Directory.Exists(settings.SpecsPath)) Directory.CreateDirectory(settings.SpecsPath); + if (!Directory.Exists(settings.GeneratedPath)) Directory.CreateDirectory(settings.GeneratedPath); } -await app.RunAsync(); \ No newline at end of file +// 프로젝트 루트 탐색 헬퍼 메서드 +string? TryFindProjectRoot(string startPath) +{ + var dir = new DirectoryInfo(startPath); + while (dir != null) + { + // 이 파일이 있는 곳을 루트(openapi-to-sdk 폴더)로 간주 + if (dir.GetFiles("Dockerfile.openapi-to-sdk").Length > 0) + { + return dir.FullName; + } + dir = dir.Parent; + } + return null; // 못 찾으면 null 반환 (현재 위치 사용) +} \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs index 2e445caa..3b4c3d6a 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs @@ -4,88 +4,73 @@ namespace McpSamples.OpenApiToSdk.HybridApp.Prompts; /// -/// This provides interfaces for SDK generation prompts. +/// Defines the interface for SDK generation prompts. /// public interface ISdkGenerationPrompt { /// - /// Gets a structured prompt that guides the LLM to generate an SDK. + /// Gets a prompt to guide the user in generating a client SDK. /// - /// The target programming language for the SDK. - /// The OpenAPI source, which can be a URL, file path, or raw content. - /// The class name for the core client class (optional). - /// The namespace for the core client class (optional). - /// Additional options for Kiota (optional). - /// A formatted string representing the SDK generation prompt. string GetSdkGenerationPrompt( + string specSource, string language, - string openApiSource, - string? className = null, - string? namespaceName = null, - string? additionalOptions = null); + string? clientClassName = "ApiClient", + string? namespaceName = "ApiSdk", + string? additionalOptions = "None"); } /// -/// This represents the prompts entity for SDK generation. +/// Represents the prompts for the OpenAPI to SDK generator. /// [McpServerPromptType] public class SdkGenerationPrompt : ISdkGenerationPrompt { /// - [McpServerPrompt(Name = "generate_sdk", Title = "Generate SDK from OpenAPI Spec")] - [Description("Returns a structured prompt that guides the LLM to generate an SDK using the 'generate_sdk' tool. It handles language normalization and input source resolution (URL vs File).")] + [McpServerPrompt(Name = "generate_sdk_prompt", Title = "Prompt for generating client SDK")] + [Description("A prompt to guide the user in generating a client SDK from an OpenAPI specification.")] public string GetSdkGenerationPrompt( - [Description("The OpenAPI source. This can be a public URL, a local file path, OR the raw content (JSON/YAML).")] string openApiSource, - [Description("The target language (e.g. csharp, go, typescript).")] string language, - [Description("The class name to use for the core client class.")] string? className = null, - [Description("The namespace to use for the core client class.")] string? namespaceName = null, - [Description("Additional options for Kiota.")] string? additionalOptions = null) - { - return $""" - You are an expert SDK generator using Microsoft Kiota. + [Description("The URL or local file path of the OpenAPI specification.")] + string specSource, + + [Description("The target programming language. Supported values: csharp, go, java, php, python, ruby, shell, swift, typescript.")] + string language, - 1. User Input Analysis - - OpenAPI Source: "{openApiSource}" - - Target Language: "{language}" - - Configuration: - - Class Name: {className ?? "Default (ApiClient)"} - - Namespace: {namespaceName ?? "Default (ApiSdk)"} - - Options: {additionalOptions ?? "None"} + [Description("The name of the generated client class. Default: 'ApiClient'.")] + string? clientClassName = "ApiClient", - --- - 2. Execution Strategy (Follow Strictly) + [Description("The namespace for the generated code. Default: 'ApiSdk'.")] + string? namespaceName = "ApiSdk", + + [Description("Any additional options for Kiota generation (e.g., --version).")] + string? additionalOptions = "None") + { + return $""" + You are an expert SDK generator using Microsoft Kiota. - Step 1: Validate & Normalize Language - Match the input to a valid Kiota identifier: [ CSharp, Go, Java, PHP, Python, Ruby, Shell, Swift, TypeScript ]. - - If a match or alias is found (e.g., "ts" -> "TypeScript", "golang" -> "Go"), use the valid identifier. - - If NO match is found (e.g., "Rust", "C++", "asdf"), STOP immediately and ask the user to provide a supported language. + Your task is to generate a client SDK based on the following inputs: + - OpenAPI Source: `{specSource}` + - Target Language: `{language}` + - Configuration: + - Class Name: {clientClassName} + - Namespace: {namespaceName} + - Additional Options: {additionalOptions} - Step 2: Resolve OpenAPI Source (CRITICAL) - The 'generate_sdk' tool accepts either a URL or Raw Content, but NOT a file path. - Analyze the [OpenAPI Source] provided above: - - - CASE A: It is a URL (starts with http/https) - - Action: Pass the URL string directly to the `specSource` argument. - - - CASE B: It looks like a File Path (e.g., "C:\specs\api.json", "./swagger.yaml") - - Action: You MUST first read the content of this file using your available tools (e.g., `filesystem` tool). - - Then, pass the file content (JSON/YAML text) to the `specSource` argument. - - If you cannot read the file, ask the user to paste the content directly. + --- + ### Execution Rules (Follow Strictly) - - CASE C: It is Raw JSON/YAML Content - - Action: Pass the content string directly to the `specSource` argument. + 1. **Validate & Normalize Language**: + The `generate_sdk` tool ONLY accepts the following lowercase language identifiers: + [ **csharp**, **go**, **java**, **php**, **python**, **ruby**, **shell**, **swift**, **typescript** ] - Step 3: Call Tool - Call the `generate_sdk` tool with the prepared arguments: - - `language`: (The normalized identifier from Step 1) - - `specSource`: (The resolved URL or Content from Step 2) - - `className`: (As provided) - - `namespaceName`: (As provided) - - `additionalOptions`: (As provided) + - If the user input is "C#", ".NET", or "csharp", you MUST use **`csharp`**. + - If the user input is "TypeScript", "ts", or "TS", you MUST use **`typescript`**. + - If the input is not in the list (e.g., "Rust", "C++"), STOP and inform the user it is not supported. - Step 4: Report Results - - If a download link is returned, display it clearly. - - If a local path is returned, provide the path. - """; + 2. **Call the Tool**: + Use the `generate_sdk` tool with the normalized language and provided parameters. + + 3. **Report**: + Provide the download link or file path returned by the tool. + """; } } \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/IOpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/IOpenApiService.cs index b9e3e504..2092aaa6 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/IOpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/IOpenApiService.cs @@ -1,43 +1,16 @@ -using McpSamples.OpenApiToSdk.HybridApp.Models; - namespace McpSamples.OpenApiToSdk.HybridApp.Services; -/// -/// This provides interfaces for the OpenAPI service. -/// public interface IOpenApiService { - /// - /// Downloads OpenAPI specification from a URL. - /// - /// The URL of the OpenAPI specification. - /// A cancellation token. - /// The content of the OpenAPI specification as a string. - Task DownloadOpenApiSpecAsync(string openApiUrl, CancellationToken cancellationToken = default); - /// /// Generates a client SDK from an OpenAPI specification. /// - /// The URL or raw content of the OpenAPI specification. + /// The URL or local file path of the OpenAPI spec. /// The target programming language for the SDK. - /// The name for the main client class (optional). - /// The namespace for the generated SDK (optional). - /// Additional options to pass to the Kiota CLI (optional). - /// An object containing the result of the SDK generation. - Task GenerateSdkAsync( - string specSource, - string language, - string? className = null, - string? namespaceName = null, - string? additionalOptions = null); - - /// - /// Runs the Kiota command-line tool with the specified arguments. - /// - /// The path to the OpenAPI specification file or a URL. - /// The target programming language. - /// The directory where the generated SDK will be saved. - /// Additional options to pass to the Kiota CLI (optional). - /// An error message if the command fails; otherwise, null. - Task RunKiotaAsync(string openApiSpecPath, string language, string outputDir, string? additionalOptions = null); + /// The name of the generated client class (default: ApiClient). + /// The namespace for the generated code (default: ApiSdk). + /// Additional Kiota command line options. + /// Cancellation token. + /// A message indicating the result path or download URL. + Task GenerateSdkAsync(string specSource, string language, string? clientClassName, string? namespaceName, string? additionalOptions, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs index 0652dade..982961f8 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs @@ -1,257 +1,135 @@ using System.Diagnostics; using System.IO.Compression; -using McpSamples.OpenApiToSdk.HybridApp.Models; -using Microsoft.AspNetCore.Http; +using McpSamples.OpenApiToSdk.HybridApp.Configurations; using Microsoft.Extensions.Logging; namespace McpSamples.OpenApiToSdk.HybridApp.Services; -/// -/// This represents the service entity for OpenAPI operations. It handles downloading OpenAPI specifications, -/// generating SDKs using Kiota, and managing temporary files. -/// -/// An instance for making HTTP requests. -/// An instance for logging. -/// An to access the current HTTP context (optional). -public class OpenApiService( - HttpClient httpClient, - ILogger logger, - IHttpContextAccessor? httpContextAccessor = null) : IOpenApiService +public class OpenApiService(OpenApiToSdkAppSettings settings, ILogger logger) : IOpenApiService { - /// - public async Task GenerateSdkAsync( - string specSource, - string language, - string? className = null, - string? namespaceName = null, - string? additionalOptions = null) + public async Task GenerateSdkAsync(string specSource, string language, string? clientClassName, string? namespaceName, string? additionalOptions, CancellationToken cancellationToken = default) { - CleanupOldFiles(); + // 0. 기본값 설정 및 옵션 처리 + var finalClassName = string.IsNullOrWhiteSpace(clientClassName) ? "ApiClient" : clientClassName; + var finalNamespace = string.IsNullOrWhiteSpace(namespaceName) ? "ApiSdk" : namespaceName; + var finalOptions = additionalOptions ?? string.Empty; - var result = new OpenApiToSdkResult(); - string kiotaInputPath; - string? tempInputFile = null; - var tempGenDir = Path.Combine(Path.GetTempPath(), "kiota_gen_" + Guid.NewGuid()); + // 1. 입력 소스 판별 (URL vs 파일 경로) + string inputPath; + bool isUrl = Uri.TryCreate(specSource, UriKind.Absolute, out var uriResult) + && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); - try + if (isUrl) { - // Auto-detects if the source is a URL or raw content. - if (IsUrl(specSource)) - { - // If it's a URL, pass it directly to Kiota. - kiotaInputPath = specSource; - logger.LogInformation("Detected URL input: {Url}", specSource); - } - else - { - // If it's raw content, save it to a temporary file and pass the path. - tempInputFile = await CreateTempFileFromContentAsync(specSource); - kiotaInputPath = tempInputFile; - logger.LogInformation("Detected raw content input. Created temp file: {Path}", tempInputFile); - } - - var optionsList = new List(); - if (!string.IsNullOrWhiteSpace(className)) - { - optionsList.Add($"--class-name \"{className}\""); - } - if (!string.IsNullOrWhiteSpace(namespaceName)) - { - optionsList.Add($"--namespace-name \"{namespaceName}\""); - } - if (!string.IsNullOrWhiteSpace(additionalOptions)) - { - optionsList.Add(additionalOptions); - } - - var combinedOptions = string.Join(" ", optionsList); - - Directory.CreateDirectory(tempGenDir); - var kiotaError = await RunKiotaAsync(kiotaInputPath, language, tempGenDir, combinedOptions); - - if (!string.IsNullOrEmpty(kiotaError)) + inputPath = specSource; + logger.LogInformation("Input is a URL: {InputPath}", inputPath); + } + else + { + if (settings.IsContainer || settings.IsAzure) { - result.ErrorMessage = kiotaError; - return result; - } - - var webRootPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot"); - var outputDir = Path.Combine(webRootPath, "generated"); - Directory.CreateDirectory(outputDir); - - var fileName = $"{language}-{DateTime.Now:yyyyMMddHHmmss}-{Guid.NewGuid().ToString()[..6]}.zip"; - var finalZipPath = Path.Combine(outputDir, fileName); - - ZipFile.CreateFromDirectory(tempGenDir, finalZipPath); + string fileName = Path.GetFileName(specSource); + inputPath = Path.Combine(settings.SpecsPath, fileName); - result.ServerFilePath = finalZipPath; - var request = httpContextAccessor?.HttpContext?.Request; - - if (request != null) - { - var baseUrl = $"{request.Scheme}://{request.Host}"; - result.ZipPath = $"{baseUrl}/generated/{fileName}"; - result.Message = $"SDK generation successful. Download link: {result.ZipPath}"; + if (!File.Exists(inputPath)) + { + var errorMsg = $"[Error] File not found in mounted volume: {inputPath}.\n" + + $"For Docker/Azure: Please ensure the spec file is uploaded/copied to the mounted 'workspace/specs' folder."; + logger.LogError(errorMsg); + return errorMsg; + } } else { - var fileUri = "file:///" + finalZipPath.Replace("\\", "/").TrimStart('/'); - result.ZipPath = fileUri; - result.Message = $"SDK generation successful! File Location: {fileUri}"; - } - } - catch (Exception ex) - { - logger.LogError(ex, "Error during SDK generation workflow."); - result.ErrorMessage = ex.Message; - } - finally - { - CleanupTempResources(tempInputFile, tempGenDir); - } - - return result; - } - - /// - /// Determines if the given string is an absolute URL. - /// - /// The string to check. - /// true if the input is a valid HTTP or HTTPS URL; otherwise, false. - private bool IsUrl(string input) - { - return Uri.TryCreate(input, UriKind.Absolute, out var uriResult) - && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); - } - - /// - /// Deletes old generated SDK files from the output directory to save space. - /// - private void CleanupOldFiles() - { - try - { - var webRootPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot"); - var outputDir = Path.Combine(webRootPath, "generated"); - - if (Directory.Exists(outputDir)) - { - var files = Directory.GetFiles(outputDir); - foreach (var file in files) + inputPath = specSource; + if (!File.Exists(inputPath)) { - if (DateTime.Now - File.GetCreationTime(file) > TimeSpan.FromHours(1)) - { - try { File.Delete(file); } - catch - { - // Ignored - } - } + var errorMsg = $"[Error] Local file not found: {inputPath}"; + logger.LogError(errorMsg); + return errorMsg; } } + logger.LogInformation("Input is a File: {InputPath}", inputPath); } - catch (Exception ex) - { - logger.LogWarning("Failed to clean up old generated files: {Message}", ex.Message); - } - } - - /// - /// Cleans up temporary files and directories created during SDK generation. - /// - /// The path to the temporary input file to delete. - /// The path to the temporary generation directory to delete. - private void CleanupTempResources(string? tempFile, string tempDir) - { - if (tempFile != null && File.Exists(tempFile)) - { - try { File.Delete(tempFile); } - catch (Exception ex) { logger.LogWarning("Failed to delete temp file: {Message}", ex.Message); } - } - - if (Directory.Exists(tempDir)) - { - try { Directory.Delete(tempDir, true); } - catch (Exception ex) { logger.LogWarning("Failed to delete temp dir: {Message}", ex.Message); } - } - } - /// - /// Creates a temporary JSON file from a string content. - /// - /// The string content to write to the file. - /// The path to the newly created temporary file. - private async Task CreateTempFileFromContentAsync(string content) - { - var tempPath = Path.GetTempFileName(); - var jsonPath = Path.ChangeExtension(tempPath, ".json"); - if (File.Exists(tempPath)) - { - File.Move(tempPath, jsonPath, true); - } - await File.WriteAllTextAsync(jsonPath, content); - return jsonPath; - } - - /// - public async Task DownloadOpenApiSpecAsync(string openApiUrl, CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(openApiUrl)) - { - throw new ArgumentException("URL is required.", nameof(openApiUrl)); - } - var response = await httpClient.GetAsync(openApiUrl, cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - } + // 2. 임시 출력 폴더 생성 + string outputId = Guid.NewGuid().ToString(); + string tempOutputPath = Path.Combine(settings.GeneratedPath, outputId); + Directory.CreateDirectory(tempOutputPath); - /// - public async Task RunKiotaAsync(string openApiSpecPath, string language, string outputDir, string? additionalOptions = null) - { try { - var arguments = $"generate -l {language} -d \"{openApiSpecPath}\" -o \"{outputDir}\" {additionalOptions}"; + // 3. Kiota 실행 + // additionalOptions가 포함된 Arguments 구성 + logger.LogInformation("Starting Kiota generation..."); var startInfo = new ProcessStartInfo { FileName = "kiota", - Arguments = arguments, + Arguments = $"generate -l {language} -c {finalClassName} -n {finalNamespace} -d \"{inputPath}\" -o \"{tempOutputPath}\" {finalOptions}", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; - logger.LogInformation("Running Kiota: kiota {Arguments}", arguments); - using var process = new Process { StartInfo = startInfo }; process.Start(); + await process.WaitForExitAsync(cancellationToken); + + if (process.ExitCode != 0) + { + string error = await process.StandardError.ReadToEndAsync(cancellationToken); + logger.LogError("Kiota generation failed: {Error}", error); + return $"[Error] Kiota generation failed:\n{error}"; + } + + // 4. Zip 압축 + string zipFileName = $"sdk-{language}-{outputId.Substring(0, 4)}.zip"; + string zipFilePath = Path.Combine(settings.GeneratedPath, zipFileName); - var timeout = TimeSpan.FromMinutes(5); - var exitTask = process.WaitForExitAsync(); + ZipFile.CreateFromDirectory(tempOutputPath, zipFilePath); + logger.LogInformation("SDK generated and zipped at: {ZipFilePath}", zipFilePath); - if (await Task.WhenAny(exitTask, Task.Delay(timeout)).ConfigureAwait(false) == exitTask) + // 5. 결과 반환 메시지 생성 + return CreateResultMessage(zipFileName, zipFilePath); + } + catch (Exception ex) + { + logger.LogError(ex, "An unexpected error occurred during SDK generation."); + return $"[Error] An unexpected error occurred: {ex.Message}"; + } + finally + { + if (Directory.Exists(tempOutputPath)) { - if (process.ExitCode != 0) + try { - var error = await process.StandardError.ReadToEndAsync(); - return $"Kiota error: {error}"; + Directory.Delete(tempOutputPath, true); } - return null; - } - else - { - try { process.Kill(entireProcessTree: true); } - catch + catch (Exception ex) { - // Ignored + logger.LogWarning(ex, "Failed to clean up temp directory: {TempPath}", tempOutputPath); } - return "Kiota execution timed out."; } } - catch (Exception ex) + } + + private string CreateResultMessage(string zipFileName, string localZipPath) + { + if (settings.IsHttpMode) + { + string downloadUrl = $"/download/{zipFileName}"; + return $"✅ SDK Generation Successful!\n\n" + + $"Download Link: {downloadUrl}\n" + + $"(Note: If accessing locally via browser, prepend your host address, e.g., http://localhost:8080{downloadUrl})"; + } + else { - return $"Kiota exception: {ex.Message}"; + return $"✅ SDK Generation Successful!\n\n" + + $"File Saved At: {localZipPath}\n\n" + + $"The file is currently in the workspace. Please check if this location is correct.\n" + + $"If the user wants the file elsewhere, please move it to the desired destination."; } } } \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs index c3719517..a020c5c8 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs @@ -1,62 +1,59 @@ using System.ComponentModel; -using McpSamples.OpenApiToSdk.HybridApp.Models; using McpSamples.OpenApiToSdk.HybridApp.Services; -using Microsoft.Extensions.Logging; using ModelContextProtocol.Server; namespace McpSamples.OpenApiToSdk.HybridApp.Tools; /// -/// Represents the tool for generating an SDK from an OpenAPI specification. +/// Defines the interface for the OpenAPI to SDK tool. /// -/// The service for handling OpenAPI operations. -/// The logger for this tool. +public interface IOpenApiToSdkTool +{ + /// + /// Generates a client SDK from an OpenAPI specification. + /// + Task GenerateSdkAsync( + string specSource, + string language, + string? clientClassName = null, + string? namespaceName = null, + string? additionalOptions = null); +} + +/// +/// Represents the tool for generating client SDKs from OpenAPI specifications. +/// +/// instance. [McpServerToolType] -public class OpenApiToSdkTool(IOpenApiService openApiService, ILogger logger) +public class OpenApiToSdkTool(IOpenApiService service) : IOpenApiToSdkTool { - /// - /// Generates a client SDK from an OpenAPI specification provided as a URL or raw content. - /// - /// The URL or raw text content of the OpenAPI specification (JSON/YAML). - /// The target language for the SDK (e.g., CSharp, Java, Python). - /// The name for the client class (optional). - /// The namespace for the generated client code (optional). - /// Additional command-line options to pass to the Kiota tool (optional). - /// An containing the result of the generation process. - [McpServerTool(Name = "generate_sdk", Title = "Generate SDK from OpenAPI")] - [Description("Generates a client SDK. Accepts either a URL or raw OpenAPI Content (JSON/YAML).")] - public async Task GenerateSdkAsync( - [Description("The OpenAPI source. Provide a URL (http://...) OR the raw content text (JSON/YAML).")] string specSource, - [Description("The target language (e.g., CSharp, Java, Python).")] string language, - [Description("The class name for the client (optional).")] string? className = null, - [Description("The namespace for the client (optional).")] string? namespaceName = null, - [Description("Additional Kiota CLI options (optional).")] string? additionalOptions = null) - { - // Validate input - if (string.IsNullOrWhiteSpace(specSource)) - { - return new OpenApiToSdkResult - { - ErrorMessage = "The 'specSource' parameter is required. It must be a URL or OpenAPI content." - }; - } - - logger.LogInformation("Generating SDK for language: {Language}", language); - - try - { - // Call the service to perform the generation - return await openApiService.GenerateSdkAsync( - specSource, - language, - className, - namespaceName, - additionalOptions); - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to execute generate_sdk tool."); - return new OpenApiToSdkResult { ErrorMessage = $"Tool execution error: {ex.Message}" }; - } - } + /// + [McpServerTool(Name = "generate_sdk", Title = "Generates a client SDK")] + [Description("Generates a client SDK from an OpenAPI specification URL or local file path.")] + public async Task GenerateSdkAsync( + [Description("The URL or local file path of the OpenAPI specification.")] + string specSource, + + [Description("The target programming language (e.g., CSharp, Python, Java, TypeScript).")] + string language, + + [Description("The name of the generated client class. Default is 'ApiClient'.")] + string? clientClassName = null, + + [Description("The namespace for the generated code. Default is 'ApiSdk'.")] + string? namespaceName = null, + + [Description("Additional Kiota command line options (e.g., --clean-output).")] + string? additionalOptions = null) + { + // Service 호출 (복잡한 파라미터 파싱 로직이 사라지고 바로 호출 가능) + var resultMessage = await service.GenerateSdkAsync( + specSource, + language, + clientClassName, + namespaceName, + additionalOptions); + + return resultMessage; + } } \ No newline at end of file From 3dd9fab51978d37a097243f93218deb3b4c9a756 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Wed, 3 Dec 2025 22:32:08 +0900 Subject: [PATCH 42/61] Update configuration and service logic for OpenAPI to SDK generator - Change server URL in mcp.json to point to the correct localhost port. - Enhance Dockerfile to include Kiota tool installation and set up workspace directories. - Modify mcp.stdio.container.json to use environment variables for volume mapping. - Update README.md with new Docker run commands reflecting workspace changes. - Add IHttpContextAccessor to OpenApiService for better HTTP context management. - Improve error handling in SDK generation process for missing spec files. - Adjust OpenApiToSdkTool description for additional command line options. -> Successfully generate zip file using Docker. --- .vscode/mcp.json | 2 +- Dockerfile.openapi-to-sdk | 28 +++++- .../.vscode/mcp.stdio.container.json | 6 +- openapi-to-sdk/README.md | 8 +- .../Program.cs | 4 + .../Services/OpenApiService.cs | 97 +++++++++++++++++-- .../Tools/OpenApiToSdkTool.cs | 2 +- 7 files changed, 128 insertions(+), 19 deletions(-) diff --git a/.vscode/mcp.json b/.vscode/mcp.json index eee37b48..c52e811a 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -2,7 +2,7 @@ "servers": { "openapi-to-sdk": { "type": "http", - "url": "http://localhost:5222/mcp" + "url": "http://localhost:8080/mcp" } } } \ No newline at end of file diff --git a/Dockerfile.openapi-to-sdk b/Dockerfile.openapi-to-sdk index 81b1a3f8..fd403246 100644 --- a/Dockerfile.openapi-to-sdk +++ b/Dockerfile.openapi-to-sdk @@ -1,14 +1,20 @@ # syntax=docker/dockerfile:1 +# ----------------------------------------------------------------------------- +# Build Stage +# ----------------------------------------------------------------------------- FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build +# 소스 코드 복사 (Shared 프로젝트 포함) COPY ./shared/McpSamples.Shared /source/shared/McpSamples.Shared COPY ./openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp /source/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp WORKDIR /source/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp +# Kiota 도구 설치 (빌드 스테이지에서 설치 후 복사) RUN dotnet tool install --global Microsoft.OpenApi.Kiota +# 아키텍처에 맞게 게시 (Publish) ARG TARGETARCH RUN case "$TARGETARCH" in \ "amd64") RID="linux-musl-x64" ;; \ @@ -17,21 +23,37 @@ RUN case "$TARGETARCH" in \ esac && \ dotnet publish -c Release -o /app -r $RID --self-contained false +# ----------------------------------------------------------------------------- +# Runtime Stage +# ----------------------------------------------------------------------------- FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final WORKDIR /app +# 1. 빌드 결과물 복사 COPY --from=build /app . +# 2. Kiota 도구 복사 및 설정 COPY --from=build /root/.dotnet/tools /opt/kiota-tools + +# [중요] 권한 설정 및 심볼릭 링크 +# - /opt/kiota-tools: 실행 가능하도록 설정 +# - kiota: 전역 경로에 심볼릭 링크 연결 RUN chmod -R 755 /opt/kiota-tools && \ ln -s /opt/kiota-tools/kiota /usr/local/bin/kiota -ENV PATH="/opt/kiota-tools:${PATH}" +# 3. [개선] Workspace 디렉터리 생성 및 권한 부여 +# - 미리 폴더를 만들어두지 않으면, 앱 실행 시 권한 오류가 날 수 있습니다. +# - $APP_UID는 .NET 이미지에 내장된 변수입니다. +RUN mkdir -p /app/workspace/generated && \ + mkdir -p /app/workspace/specs && \ + chown -R $APP_UID:$APP_UID /app/workspace -RUN mkdir -p /app/wwwroot/generated && \ - chown -R $APP_UID:$APP_UID /app/wwwroot +# PATH 환경 변수 설정 +ENV PATH="/opt/kiota-tools:${PATH}" +# 4. 보안을 위해 비-루트 사용자 전환 USER $APP_UID +# 실행 ENTRYPOINT ["dotnet", "McpSamples.OpenApiToSdk.HybridApp.dll"] \ No newline at end of file diff --git a/openapi-to-sdk/.vscode/mcp.stdio.container.json b/openapi-to-sdk/.vscode/mcp.stdio.container.json index 9541a03f..23a0a217 100644 --- a/openapi-to-sdk/.vscode/mcp.stdio.container.json +++ b/openapi-to-sdk/.vscode/mcp.stdio.container.json @@ -7,8 +7,10 @@ "run", "-i", "--rm", - "--volume", - "${workspaceFolder}/generated:/app/wwwroot/generated", + "-v", + "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace", + "-e", + "HOST_ROOT_PATH=${env:REPOSITORY_ROOT}/openapi-to-sdk", "openapi-to-sdk:latest" ] } diff --git a/openapi-to-sdk/README.md b/openapi-to-sdk/README.md index b4320661..8478c8ca 100644 --- a/openapi-to-sdk/README.md +++ b/openapi-to-sdk/README.md @@ -82,13 +82,13 @@ OpenAPI to SDK MCP server includes: 1. Run the MCP server app in a container. ```bash - docker run -i --rm -p 8080:8080 openapi-to-sdk:latest + docker run -i --rm -p 8080:8080 -v "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace" -e HOST_ROOT_PATH="$REPOSITORY_ROOT" openapi-to-sdk:latest ``` Alternatively, use the container image from the container registry. ```bash - docker run -i --rm -p 8080:8080 ghcr.io/microsoft/mcp-dotnet-samples/openapi-to-sdk:latest + docker run -i --rm -p 8080:8080 -v "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace" -e HOST_ROOT_PATH="$REPOSITORY_ROOT" ghcr.io/microsoft/mcp-dotnet-samples/openapi-to-sdk:latest ``` **Parameters**: @@ -99,12 +99,12 @@ OpenAPI to SDK MCP server includes: ```bash # use local container image - docker run -i --rm -p 8080:8080 openapi-to-sdk:latest --http + docker run -i --rm -p 8080:8080 -v "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace" -e HOST_ROOT_PATH="$REPOSITORY_ROOT" openapi-to-sdk:latest --http ``` ```bash # use container image from the container registry - docker run -i --rm -p 8080:8080 ghcr.io/microsoft/mcp-dotnet-samples/openapi-to-sdk:latest --http + docker run -i --rm -p 8080:8080 -v "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace" -e HOST_ROOT_PATH="$REPOSITORY_ROOT" ghcr.io/microsoft/mcp-dotnet-samples/openapi-to-sdk:latest --http ``` #### On Azure diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs index 8eac7471..f2eda2d0 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs @@ -18,6 +18,9 @@ // appsettings.json 로드 및 객체 바인딩 처리 builder.Services.AddAppSettings(builder.Configuration, args); +// [추가] HttpContext에 접근하기 위해 등록 (HTTP 모드일 때만 필수지만, 안전하게 항상 등록해도 무방함) +builder.Services.AddHttpContextAccessor(); + // 4. 서비스 등록 var options = new JsonSerializerOptions { @@ -62,6 +65,7 @@ RequestPath = "/download", ServeUnknownFileTypes = true }); + } // 6. 앱 실행 diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs index 982961f8..1a74f003 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs @@ -1,15 +1,18 @@ using System.Diagnostics; using System.IO.Compression; using McpSamples.OpenApiToSdk.HybridApp.Configurations; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace McpSamples.OpenApiToSdk.HybridApp.Services; -public class OpenApiService(OpenApiToSdkAppSettings settings, ILogger logger) : IOpenApiService +public class OpenApiService(OpenApiToSdkAppSettings settings, IHttpContextAccessor httpContextAccessor, ILogger logger) : IOpenApiService { public async Task GenerateSdkAsync(string specSource, string language, string? clientClassName, string? namespaceName, string? additionalOptions, CancellationToken cancellationToken = default) { // 0. 기본값 설정 및 옵션 처리 + if (string.IsNullOrWhiteSpace(specSource)) + throw new ArgumentException("Spec source cannot be empty.", nameof(specSource)); var finalClassName = string.IsNullOrWhiteSpace(clientClassName) ? "ApiClient" : clientClassName; var finalNamespace = string.IsNullOrWhiteSpace(namespaceName) ? "ApiSdk" : namespaceName; var finalOptions = additionalOptions ?? string.Empty; @@ -28,15 +31,52 @@ public async Task GenerateSdkAsync(string specSource, string language, s { if (settings.IsContainer || settings.IsAzure) { + // 컨테이너/Azure 환경에서는 마운트된 볼륨의 specs 폴더를 확인합니다. + // 리눅스 컨테이너에서 윈도우 경로(\)가 들어올 경우를 대비해 파일명만 확실하게 추출합니다. string fileName = Path.GetFileName(specSource); + if (fileName.Contains('\\')) + { + fileName = fileName.Split('\\').Last(); + } + inputPath = Path.Combine(settings.SpecsPath, fileName); + // 2. 파일이 마운트된 경로에 없으면 에이전트에게 복사를 요청합니다. (에러 처리 X) if (!File.Exists(inputPath)) { - var errorMsg = $"[Error] File not found in mounted volume: {inputPath}.\n" + - $"For Docker/Azure: Please ensure the spec file is uploaded/copied to the mounted 'workspace/specs' folder."; - logger.LogError(errorMsg); - return errorMsg; + // 기본 안내 경로 (환경변수 누락 시 대비) + string targetHostPath = $"workspace/specs/{fileName}"; + + // 도커 실행 시 주입된 HOST_ROOT_PATH 환경 변수 확인 + // 옵션: -e HOST_ROOT_PATH=${env:REPOSITORY_ROOT}/openapi-to-sdk + string? hostRootPath = Environment.GetEnvironmentVariable("HOST_ROOT_PATH"); + + if (!string.IsNullOrEmpty(hostRootPath)) + { + // 1. 경로 정규화: 윈도우(\)와 리눅스(/) 구분자를 모두 포워드 슬래시('/')로 통일 + // 이렇게 하면 어떤 OS의 Agent라도 경로를 오해 없이 인식합니다. + string normalizedHostRoot = hostRootPath.Replace('\\', '/').TrimEnd('/'); + + // 2. 최종 호스트 타겟 경로 조립 + // 예: D:/Projects/openapi-to-sdk/workspace/specs/petstore.json + targetHostPath = $"{normalizedHostRoot}/workspace/specs/{fileName}"; + } + + // [핵심] 에러가 아닌 '조치 요청' 메시지 반환 + // 문구가 [Error]로 시작하지 않으므로 Agent는 이를 '성공적인 응답'으로 인식하고, + // 내용에 포함된 지시사항(파일 복사)을 수행하려고 시도합니다. + return $""" + ⚠️ Action Required: File Synchronization Needed + + The file '{fileName}' is not currently in the shared volume visible to the container. + To proceed with SDK generation, I need access to this file. + + Please perform the following steps using your filesystem tools: + 1. COPY the file from: `{specSource}` + 2. PASTE it to: `{targetHostPath}` + + After copying the file, please call this 'generate_sdk' tool again with the same arguments. + """; } } else @@ -119,13 +159,54 @@ private string CreateResultMessage(string zipFileName, string localZipPath) { if (settings.IsHttpMode) { - string downloadUrl = $"/download/{zipFileName}"; + string relativePath = $"/download/{zipFileName}"; + string downloadUrl; + + // 현재 요청(Request) 정보 가져오기 + var request = httpContextAccessor.HttpContext?.Request; + + if (request != null) + { + // Local, Docker, Azure 모두 현재 접속된 Host(도메인+포트)를 기준으로 URL 생성 + // 예: http://localhost:5222, https://myapp.azurecontainerapps.io 등 + string baseUrl = $"{request.Scheme}://{request.Host}"; + downloadUrl = $"{baseUrl}{relativePath}"; + } + else + { + // HttpContext가 없는 예외적인 경우 (Fallback) + // Azure나 Docker는 보통 포트 8080, 로컬은 5222 등 다양하므로 상대 경로만 제공 + downloadUrl = relativePath; + } + return $"✅ SDK Generation Successful!\n\n" + - $"Download Link: {downloadUrl}\n" + - $"(Note: If accessing locally via browser, prepend your host address, e.g., http://localhost:8080{downloadUrl})"; + $"Download Link: {downloadUrl}"; } else { + string finalPath = localZipPath; + + if (settings.IsContainer) // Docker Stdio 모드 + { + // 호스트 경로 환경 변수 읽기 + // 예: "D:/KNU/3-2/CDP1/openapi-to-sdk" + string? hostRootPath = Environment.GetEnvironmentVariable("HOST_ROOT_PATH"); + + if (!string.IsNullOrEmpty(hostRootPath)) + { + // 1. 컨테이너 경로의 시작인 /app을 제외합니다. + // finalPath = /app/workspace/... + string relativePathFromApp = finalPath.Substring("/app".Length).TrimStart('/'); + + // 2. 호스트 경로의 끝에 있는 슬래시(/)나 역슬래시(\)를 정리합니다. + string hostPathNormalized = hostRootPath.TrimEnd('/', '\\'); + + // 3. 크로스 플랫폼 호환성을 위해 최종 경로를 포워드 슬래시(/)로 연결합니다. + // Path.Combine 대신 string concatenation을 사용하여 OS 종속성을 제거합니다. + finalPath = $"{hostPathNormalized}/{relativePathFromApp}"; + } + } + // Stdio 모드 (기존 동일) return $"✅ SDK Generation Successful!\n\n" + $"File Saved At: {localZipPath}\n\n" + $"The file is currently in the workspace. Please check if this location is correct.\n" + diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs index a020c5c8..782ba30e 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs @@ -43,7 +43,7 @@ public async Task GenerateSdkAsync( [Description("The namespace for the generated code. Default is 'ApiSdk'.")] string? namespaceName = null, - [Description("Additional Kiota command line options (e.g., --clean-output).")] + [Description("Additional Kiota command line options (e.g., --version).")] string? additionalOptions = null) { // Service 호출 (복잡한 파라미터 파싱 로직이 사라지고 바로 호출 가능) From c9c97ffe25ba96442ac211d4c38c3ac8f238bebf Mon Sep 17 00:00:00 2001 From: x-or-b Date: Thu, 4 Dec 2025 03:35:08 +0900 Subject: [PATCH 43/61] Enhance OpenAPI to SDK generator: add file upload endpoint, improve Azure handling, and update SDK generation prompts --- .vscode/mcp.json | 14 +++- openapi-to-sdk/infra/resources.bicep | 82 +++++++++++++++++-- .../Program.cs | 24 +++++- .../Prompts/SdkGenerationPrompt.cs | 27 ++++-- .../Services/OpenApiService.cs | 81 ++++++++++++------ 5 files changed, 187 insertions(+), 41 deletions(-) diff --git a/.vscode/mcp.json b/.vscode/mcp.json index c52e811a..81662075 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,8 +1,18 @@ { "servers": { "openapi-to-sdk": { - "type": "http", - "url": "http://localhost:8080/mcp" + "type": "stdio", + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-v", + "${env:REPOSITORY_ROOT}/openapi-to-sdk/workspace:/app/workspace", + "-e", + "HOST_ROOT_PATH=${env:REPOSITORY_ROOT}/openapi-to-sdk", + "openapi-to-sdk:latest" + ] } } } \ No newline at end of file diff --git a/openapi-to-sdk/infra/resources.bicep b/openapi-to-sdk/infra/resources.bicep index cc6d1e6f..b404389c 100644 --- a/openapi-to-sdk/infra/resources.bicep +++ b/openapi-to-sdk/infra/resources.bicep @@ -12,6 +12,37 @@ param principalId string var abbrs = loadJsonContent('./abbreviations.json') var resourceToken = uniqueString(subscription().id, resourceGroup().id, location) +// [추가] 1. 스토리지 계정 및 파일 공유 생성 +resource storage 'Microsoft.Storage/storageAccounts@2025-01-01' = { + name: '${abbrs.storageStorageAccounts}${resourceToken}' + location: location + tags: tags + kind: 'StorageV2' + sku: { + name: 'Standard_LRS' + } + properties: { + minimumTlsVersion: 'TLS1_2' + allowBlobPublicAccess: false + publicNetworkAccess: 'Enabled' + } +} + +resource storageFileService 'Microsoft.Storage/storageAccounts/fileServices@2025-01-01' = { + parent: storage + name: 'default' +} + +resource storageFileShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2025-01-01' = { + parent: storageFileService + name: 'workspace' + properties: { + accessTier: 'TransactionOptimized' + shareQuota: 1024 + enabledProtocols: 'SMB' + } +} + // Monitor application with Azure Monitor module monitoring 'br/public:avm/ptn/azd/monitoring:0.1.0' = { name: 'monitoring' @@ -54,24 +85,46 @@ module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.4.5 } } +// [추가] 2. 환경과 스토리지 연결 +resource env 'Microsoft.App/managedEnvironments@2025-01-01' existing = { + name: '${abbrs.appManagedEnvironments}${resourceToken}' +} + +resource envStorage 'Microsoft.App/managedEnvironments/storages@2025-01-01' = { + parent: env + name: 'workspace' + properties: { + azureFile: { + accountName: storage.name + accountKey: storage.listKeys().keys[0].value + shareName: storageFileShare.name + accessMode: 'ReadWrite' + } + } + dependsOn: [ + containerAppsEnvironment + ] +} + // User assigned identity module mcpOpenApiToSdkIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { name: 'mcpOpenApiToSdkIdentity' params: { - name: '${abbrs.managedIdentityUserAssignedIdentities}mcpopenapitosdk-${resourceToken}' + name: '${abbrs.managedIdentityUserAssignedIdentities}mcp-openapi-to-sdk-${resourceToken}' location: location } } -// Azure Container Apps +// Azure Container Apps - Image Fetching module mcpOpenApiToSdkFetchLatestImage './modules/fetch-container-image.bicep' = { - name: 'mcpOpenApiToSdk-fetch-image' + name: 'mcpOpenApiToSdkFetchLatestImage' params: { exists: mcpOpenApiToSdkExists name: 'openapi-to-sdk' } } +// Azure Container Apps - Main App module mcpOpenApiToSdk 'br/public:avm/res/app/container-app:0.8.0' = { name: 'mcpOpenApiToSdk' params: { @@ -83,6 +136,16 @@ module mcpOpenApiToSdk 'br/public:avm/res/app/container-app:0.8.0' = { secureList: [ ] } + + // [추가] 3. 볼륨 정의 (위에서 만든 envStorage의 이름을 참조) + volumes: [ + { + name: 'workspace-vol' // 앱 내부에서 쓸 볼륨 식별자 + storageType: 'AzureFile' + storageName: 'workspace' // envStorage 리소스의 name과 일치해야 함 + } + ] + containers: [ { image: mcpOpenApiToSdkFetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' @@ -108,9 +171,16 @@ module mcpOpenApiToSdk 'br/public:avm/res/app/container-app:0.8.0' = { args: [ '--http' ] + + // [추가] 4. 컨테이너 내부 경로에 마운트 + volumeMounts: [ + { + volumeName: 'workspace-vol' + mountPath: '/app/workspace' + } + ] } ] - managedIdentities: { systemAssigned: false userAssignedResourceIds: [ @@ -142,4 +212,6 @@ module mcpOpenApiToSdk 'br/public:avm/res/app/container-app:0.8.0' = { output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer output AZURE_RESOURCE_MCP_OPENAPI_TO_SDK_ID string = mcpOpenApiToSdk.outputs.resourceId output AZURE_RESOURCE_MCP_OPENAPI_TO_SDK_NAME string = mcpOpenApiToSdk.outputs.name -output AZURE_RESOURCE_MCP_OPENAPI_TO_SDK_FQDN string = mcpOpenApiToSdk.outputs.fqdn \ No newline at end of file +output AZURE_RESOURCE_MCP_OPENAPI_TO_SDK_FQDN string = mcpOpenApiToSdk.outputs.fqdn +output AZURE_STORAGE_ACCOUNT_NAME string = storage.name +output AZURE_FILE_SHARE_NAME string = storageFileShare.name \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs index f2eda2d0..728fa0bf 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs @@ -66,6 +66,29 @@ ServeUnknownFileTypes = true }); + // [신규] 파일 업로드 엔드포인트 추가 (/upload) + webApp.MapPost("/upload", async (IFormFile file, OpenApiToSdkAppSettings settings) => + { + if (file == null || file.Length == 0) + return Results.BadRequest("No file uploaded."); + + // 저장 경로: workspace/specs/{파일명} + if (!Directory.Exists(settings.SpecsPath)) + Directory.CreateDirectory(settings.SpecsPath); + + // 보안상 파일명만 추출 (경로 조작 방지) + var fileName = Path.GetFileName(file.FileName); + var filePath = Path.Combine(settings.SpecsPath, fileName); + + using (var stream = new FileStream(filePath, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + return Results.Ok(new { Message = "File uploaded successfully.", SavedPath = filePath }); + }) + .DisableAntiforgery(); // 외부(Agent/Curl)에서 호출하기 쉽도록 CSRF 비활성화 + } // 6. 앱 실행 @@ -111,7 +134,6 @@ void InitializeRuntimeSettings(OpenApiToSdkAppSettings settings, bool isHttp) settings.IsContainer = isContainer; settings.IsAzure = isAzure; - // 필수 폴더 생성 if (!Directory.Exists(settings.WorkspacePath)) Directory.CreateDirectory(settings.WorkspacePath); if (!Directory.Exists(settings.SpecsPath)) Directory.CreateDirectory(settings.SpecsPath); if (!Directory.Exists(settings.GeneratedPath)) Directory.CreateDirectory(settings.GeneratedPath); diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs index 3b4c3d6a..387562e2 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs @@ -32,7 +32,7 @@ public string GetSdkGenerationPrompt( [Description("The URL or local file path of the OpenAPI specification.")] string specSource, - [Description("The target programming language. Supported values: csharp, go, java, php, python, ruby, shell, swift, typescript.")] + [Description("The target programming language. Supported values: CSharp, Java, TypeScript, PHP, Python, Go, Ruby, Dart, HTTP.")] string language, [Description("The name of the generated client class. Default: 'ApiClient'.")] @@ -58,16 +58,27 @@ public string GetSdkGenerationPrompt( --- ### Execution Rules (Follow Strictly) - 1. **Validate & Normalize Language**: - The `generate_sdk` tool ONLY accepts the following lowercase language identifiers: - [ **csharp**, **go**, **java**, **php**, **python**, **ruby**, **shell**, **swift**, **typescript** ] + 1. **Smart Language Normalization**: + The `generate_sdk` tool ONLY accepts the following language identifiers: + [ CSharp, Java, TypeScript, PHP, Python, Go, Ruby, Dart, HTTP ] - - If the user input is "C#", ".NET", or "csharp", you MUST use **`csharp`**. - - If the user input is "TypeScript", "ts", or "TS", you MUST use **`typescript`**. - - If the input is not in the list (e.g., "Rust", "C++"), STOP and inform the user it is not supported. + You MUST intelligently map the user's input to one of these valid identifiers. + + - **Handle Aliases & Variations**: + - "C#", "c#", ".NET", "dotnet", "chsarp" (typo) -> Use **CSharp** + - "TS", "Ts", "ts", "node", "typoscript" (typo) -> Use **TypeScript** + - "Golang", "Goo" (typo) -> Use **Go** + - "py", "pyton" (typo), "python3" -> Use **Python** + - "jav", "Jave" (typo) -> Use **Java** + + - **Auto-Correction**: + - If the user makes a minor typo or uses a common abbreviation, automatically correct it to the nearest valid identifier from the list above. + + - **Validation**: + - If the input refers to a completely unsupported language (e.g., "Rust", "C++", "Assembly"), STOP and politely inform the user that it is not currently supported by Kiota. 2. **Call the Tool**: - Use the `generate_sdk` tool with the normalized language and provided parameters. + Use the `generate_sdk` tool with the **normalized lowercase language** (from Step 1) and provided parameters. 3. **Report**: Provide the download link or file path returned by the tool. diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs index 1a74f003..a2fe032f 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs @@ -44,39 +44,70 @@ public async Task GenerateSdkAsync(string specSource, string language, s // 2. 파일이 마운트된 경로에 없으면 에이전트에게 복사를 요청합니다. (에러 처리 X) if (!File.Exists(inputPath)) { - // 기본 안내 경로 (환경변수 누락 시 대비) - string targetHostPath = $"workspace/specs/{fileName}"; + if (settings.IsAzure) // [수정] Azure 환경 처리: HTTP 업로드 안내 + { + // 현재 서버의 업로드 URL 계산 + string uploadUrl = "/upload"; // Fallback + var request = httpContextAccessor.HttpContext?.Request; + if (request != null) + { + // 예: https://myapp.azurecontainerapps.io/upload + uploadUrl = $"{request.Scheme}://{request.Host}/upload"; + } - // 도커 실행 시 주입된 HOST_ROOT_PATH 환경 변수 확인 - // 옵션: -e HOST_ROOT_PATH=${env:REPOSITORY_ROOT}/openapi-to-sdk - string? hostRootPath = Environment.GetEnvironmentVariable("HOST_ROOT_PATH"); + // Agent에게 'curl' 명령어를 실행하라고 지시 + return $""" + ⚠️ Action Required: File Upload Needed (Azure) - if (!string.IsNullOrEmpty(hostRootPath)) - { - // 1. 경로 정규화: 윈도우(\)와 리눅스(/) 구분자를 모두 포워드 슬래시('/')로 통일 - // 이렇게 하면 어떤 OS의 Agent라도 경로를 오해 없이 인식합니다. - string normalizedHostRoot = hostRootPath.Replace('\\', '/').TrimEnd('/'); + I am running in an Azure environment and cannot access your local file `{specSource}` 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 + + curl -F "file=@{specSource}" {uploadUrl} + + ### 💻 Windows (PowerShell/CMD) - // 2. 최종 호스트 타겟 경로 조립 - // 예: D:/Projects/openapi-to-sdk/workspace/specs/petstore.json - targetHostPath = $"{normalizedHostRoot}/workspace/specs/{fileName}"; + curl.exe -F "file=@{specSource}" {uploadUrl} + + After the command completes successfully, please call this `generate_sdk` tool again. + """; } + if (settings.IsContainer) + { + // 기본 안내 경로 (환경변수 누락 시 대비) + string targetHostPath = $"workspace/specs/{fileName}"; + + // 도커 실행 시 주입된 HOST_ROOT_PATH 환경 변수 확인 + // 옵션: -e HOST_ROOT_PATH=${env:REPOSITORY_ROOT}/openapi-to-sdk + string? hostRootPath = Environment.GetEnvironmentVariable("HOST_ROOT_PATH"); - // [핵심] 에러가 아닌 '조치 요청' 메시지 반환 - // 문구가 [Error]로 시작하지 않으므로 Agent는 이를 '성공적인 응답'으로 인식하고, - // 내용에 포함된 지시사항(파일 복사)을 수행하려고 시도합니다. - return $""" - ⚠️ Action Required: File Synchronization Needed + if (!string.IsNullOrEmpty(hostRootPath)) + { + // 1. 경로 정규화: 윈도우(\)와 리눅스(/) 구분자를 모두 포워드 슬래시('/')로 통일 + string normalizedHostRoot = hostRootPath.Replace('\\', '/').TrimEnd('/'); - The file '{fileName}' is not currently in the shared volume visible to the container. - To proceed with SDK generation, I need access to this file. + // 2. 최종 호스트 타겟 경로 조립 + // 예: D:/Projects/openapi-to-sdk/workspace/specs/petstore.json + targetHostPath = $"{normalizedHostRoot}/workspace/specs/{fileName}"; + } - Please perform the following steps using your filesystem tools: - 1. COPY the file from: `{specSource}` - 2. PASTE it to: `{targetHostPath}` + // 에러가 아닌 '조치 요청' 메시지 반환 + return $""" + ⚠️ Action Required: File Synchronization Needed - After copying the file, please call this 'generate_sdk' tool again with the same arguments. - """; + The file '{fileName}' is not currently in the shared volume visible to the container. + To proceed with SDK generation, I need access to this file. + + Please perform the following steps using your filesystem tools: + 1. COPY the file from: `{specSource}` + 2. PASTE it to: `{targetHostPath}` + + After copying the file, please call this 'generate_sdk' tool again with the same arguments. + """; + } } } else From df15ec1947132a8665ba7fa18a0afafc92a4fc31 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Thu, 4 Dec 2025 04:36:13 +0900 Subject: [PATCH 44/61] Refactor output path handling in SDK generation: update environment variables and improve error messaging for invalid options --- .vscode/mcp.json | 4 ++-- .../.vscode/mcp.stdio.container.json | 4 ++-- .../Prompts/SdkGenerationPrompt.cs | 14 ++++++++--- .../Services/OpenApiService.cs | 23 ++++++++++++++----- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 81662075..09cf41d2 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -8,9 +8,9 @@ "-i", "--rm", "-v", - "${env:REPOSITORY_ROOT}/openapi-to-sdk/workspace:/app/workspace", + "${workspaceFolder}/openapi-to-sdk/workspace:/app/workspace", "-e", - "HOST_ROOT_PATH=${env:REPOSITORY_ROOT}/openapi-to-sdk", + "HOST_ROOT_PATH=${workspaceFolder}/openapi-to-sdk", "openapi-to-sdk:latest" ] } diff --git a/openapi-to-sdk/.vscode/mcp.stdio.container.json b/openapi-to-sdk/.vscode/mcp.stdio.container.json index 23a0a217..09cf41d2 100644 --- a/openapi-to-sdk/.vscode/mcp.stdio.container.json +++ b/openapi-to-sdk/.vscode/mcp.stdio.container.json @@ -8,9 +8,9 @@ "-i", "--rm", "-v", - "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace", + "${workspaceFolder}/openapi-to-sdk/workspace:/app/workspace", "-e", - "HOST_ROOT_PATH=${env:REPOSITORY_ROOT}/openapi-to-sdk", + "HOST_ROOT_PATH=${workspaceFolder}/openapi-to-sdk", "openapi-to-sdk:latest" ] } diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs index 387562e2..91de4556 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs @@ -77,10 +77,18 @@ You MUST intelligently map the user's input to one of these valid identifiers. - **Validation**: - If the input refers to a completely unsupported language (e.g., "Rust", "C++", "Assembly"), STOP and politely inform the user that it is not currently supported by Kiota. - 2. **Call the Tool**: - Use the `generate_sdk` tool with the **normalized lowercase language** (from Step 1) and provided parameters. + 2. **Handle Output Path**: + - The `generate_sdk` tool manages the output path internally to create a ZIP file. + - **NEVER** pass `-o` or `--output` in the `additionalOptions` argument, even if the user asks to save it to a specific location (e.g., "Generate to D:/Work"). + - Instead, follow this workflow: + 1. Call `generate_sdk` WITHOUT the output path option. + 2. Once the tool returns the ZIP file path (or download link), tell the user: "I have generated the SDK. Would you like me to move/extract it to [User's Requested Path]?" + 3. If the user agrees, use your filesystem tools to move the file. + + 3. **Call the Tool**: + Use the `generate_sdk` tool with the normalized language and filtered options (excluding -o). - 3. **Report**: + 4. **Report**: Provide the download link or file path returned by the tool. """; } diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs index a2fe032f..793416b1 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs @@ -17,6 +17,19 @@ public async Task GenerateSdkAsync(string specSource, string language, s var finalNamespace = string.IsNullOrWhiteSpace(namespaceName) ? "ApiSdk" : namespaceName; var finalOptions = additionalOptions ?? string.Empty; + if (finalOptions.Contains("-o ") || finalOptions.Contains("--output ")) + { + // -o 옵션 또는 --output 옵션이 포함된 경우 에러로 반환하여 에이전트에게 "옵션 빼고 다시 요청해"라고 가르침 (방어 코드) + return """ + ⚠️ Input Error: Invalid Option Detected + + Please DO NOT include the `-o` or `--output` option in 'additionalOptions'. + The output path is managed automatically by the server. + + If you want to save the file to a specific location, please generate it first, and then move the resulting ZIP file to your desired destination. + """; + } + // 1. 입력 소스 판별 (URL vs 파일 경로) string inputPath; bool isUrl = Uri.TryCreate(specSource, UriKind.Absolute, out var uriResult) @@ -101,11 +114,9 @@ I am running in an Azure environment and cannot access your local file `{specSou The file '{fileName}' is not currently in the shared volume visible to the container. To proceed with SDK generation, I need access to this file. - Please perform the following steps using your filesystem tools: - 1. COPY the file from: `{specSource}` - 2. PASTE it to: `{targetHostPath}` - - After copying the file, please call this 'generate_sdk' tool again with the same arguments. + EXECUTION PLAN (Follow Strictly): + 1. COPY the file from `{specSource}` to `{targetHostPath}`. + 2. RETRY the `generate_sdk` tool immediately with the same arguments. """; } } @@ -237,7 +248,7 @@ private string CreateResultMessage(string zipFileName, string localZipPath) finalPath = $"{hostPathNormalized}/{relativePathFromApp}"; } } - // Stdio 모드 (기존 동일) + // Stdio 모드 return $"✅ SDK Generation Successful!\n\n" + $"File Saved At: {localZipPath}\n\n" + $"The file is currently in the workspace. Please check if this location is correct.\n" + From d21ae440f4fd8866389be9a97fe9372553f4db50 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Thu, 4 Dec 2025 20:36:08 +0900 Subject: [PATCH 45/61] Remove TODO file and update comments for clarity in SDK generation and service classes --- openapi-to-sdk/TODO | 0 .../Configurations/OpenApiToSdkAppSettings.cs | 2 +- .../Prompts/SdkGenerationPrompt.cs | 10 +++++----- .../Services/OpenApiService.cs | 12 ++++++------ .../Tools/OpenApiToSdkTool.cs | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) delete mode 100644 openapi-to-sdk/TODO diff --git a/openapi-to-sdk/TODO b/openapi-to-sdk/TODO deleted file mode 100644 index e69de29b..00000000 diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Configurations/OpenApiToSdkAppSettings.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Configurations/OpenApiToSdkAppSettings.cs index 73966819..c63b64e9 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Configurations/OpenApiToSdkAppSettings.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Configurations/OpenApiToSdkAppSettings.cs @@ -18,7 +18,7 @@ public class OpenApiToSdkAppSettings : AppSettings }; // -------------------------------------------------------- - // Runtime Configurations (Program.cs에서 계산 후 할당될 값들) + // Runtime Configurations (Values assigned after calculation in Program.cs) // -------------------------------------------------------- /// diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs index 91de4556..bdf4b5d4 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs @@ -65,11 +65,11 @@ public string GetSdkGenerationPrompt( You MUST intelligently map the user's input to one of these valid identifiers. - **Handle Aliases & Variations**: - - "C#", "c#", ".NET", "dotnet", "chsarp" (typo) -> Use **CSharp** - - "TS", "Ts", "ts", "node", "typoscript" (typo) -> Use **TypeScript** - - "Golang", "Goo" (typo) -> Use **Go** - - "py", "pyton" (typo), "python3" -> Use **Python** - - "jav", "Jave" (typo) -> Use **Java** + - "C#", "c#", ".NET", "dotnet", "chsarp" (typo) -> Use CSharp + - "TS", "Ts", "ts", "node", "typoscript" (typo) -> Use TypeScript + - "Golang", "Goo" (typo) -> Use Go + - "py", "pyton" (typo), "python3" -> Use Python + - "jav", "Jave" (typo) -> Use Java - **Auto-Correction**: - If the user makes a minor typo or uses a common abbreviation, automatically correct it to the nearest valid identifier from the list above. diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs index 793416b1..07c974b6 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs @@ -21,7 +21,7 @@ public async Task GenerateSdkAsync(string specSource, string language, s { // -o 옵션 또는 --output 옵션이 포함된 경우 에러로 반환하여 에이전트에게 "옵션 빼고 다시 요청해"라고 가르침 (방어 코드) return """ - ⚠️ Input Error: Invalid Option Detected + Input Error: Invalid Option Detected Please DO NOT include the `-o` or `--output` option in 'additionalOptions'. The output path is managed automatically by the server. @@ -70,18 +70,18 @@ The output path is managed automatically by the server. // Agent에게 'curl' 명령어를 실행하라고 지시 return $""" - ⚠️ Action Required: File Upload Needed (Azure) + Action Required: File Upload Needed (Azure) I am running in an Azure environment and cannot access your local file `{specSource}` 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 + ### macOS & Linux curl -F "file=@{specSource}" {uploadUrl} - ### 💻 Windows (PowerShell/CMD) + ### Windows (PowerShell/CMD) curl.exe -F "file=@{specSource}" {uploadUrl} @@ -109,7 +109,7 @@ I am running in an Azure environment and cannot access your local file `{specSou // 에러가 아닌 '조치 요청' 메시지 반환 return $""" - ⚠️ Action Required: File Synchronization Needed + Action Required: File Synchronization Needed The file '{fileName}' is not currently in the shared volume visible to the container. To proceed with SDK generation, I need access to this file. @@ -249,7 +249,7 @@ private string CreateResultMessage(string zipFileName, string localZipPath) } } // Stdio 모드 - return $"✅ SDK Generation Successful!\n\n" + + return $"SDK Generation Successful!\n\n" + $"File Saved At: {localZipPath}\n\n" + $"The file is currently in the workspace. Please check if this location is correct.\n" + $"If the user wants the file elsewhere, please move it to the desired destination."; diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs index 782ba30e..61340186 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs @@ -46,7 +46,7 @@ public async Task GenerateSdkAsync( [Description("Additional Kiota command line options (e.g., --version).")] string? additionalOptions = null) { - // Service 호출 (복잡한 파라미터 파싱 로직이 사라지고 바로 호출 가능) + // Service 호출 var resultMessage = await service.GenerateSdkAsync( specSource, language, From b12d200e41808d793d544184b5642e89c0daa094 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Thu, 4 Dec 2025 20:39:35 +0900 Subject: [PATCH 46/61] Remove emoji from SDK generation success message for consistency --- .../Services/OpenApiService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs index 07c974b6..e2f42d36 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs @@ -221,7 +221,7 @@ private string CreateResultMessage(string zipFileName, string localZipPath) downloadUrl = relativePath; } - return $"✅ SDK Generation Successful!\n\n" + + return $"SDK Generation Successful!\n\n" + $"Download Link: {downloadUrl}"; } else From c09d7251dcd394ebea2406e36e969ec74407444e Mon Sep 17 00:00:00 2001 From: x-or-b Date: Thu, 4 Dec 2025 20:55:05 +0900 Subject: [PATCH 47/61] Update HOST_ROOT_PATH environment variable comment for clarity in Docker execution --- .../Services/OpenApiService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs index e2f42d36..8a4da1cc 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs @@ -94,7 +94,7 @@ I am running in an Azure environment and cannot access your local file `{specSou string targetHostPath = $"workspace/specs/{fileName}"; // 도커 실행 시 주입된 HOST_ROOT_PATH 환경 변수 확인 - // 옵션: -e HOST_ROOT_PATH=${env:REPOSITORY_ROOT}/openapi-to-sdk + // 옵션: -e HOST_ROOT_PATH=${workspaceFolder}/openapi-to-sdk string? hostRootPath = Environment.GetEnvironmentVariable("HOST_ROOT_PATH"); if (!string.IsNullOrEmpty(hostRootPath)) From e232995cbff12715a54d4579c5fb6fcdabafca6f Mon Sep 17 00:00:00 2001 From: x-or-b Date: Thu, 4 Dec 2025 21:20:03 +0900 Subject: [PATCH 48/61] Enhance documentation and comments across multiple files for clarity and consistency in OpenAPI to SDK generation --- openapi-to-sdk/infra/resources.bicep | 12 ++-- .../Program.cs | 52 +++++--------- .../Services/OpenApiService.cs | 71 +++++++++---------- .../Tools/OpenApiToSdkTool.cs | 1 - 4 files changed, 56 insertions(+), 80 deletions(-) diff --git a/openapi-to-sdk/infra/resources.bicep b/openapi-to-sdk/infra/resources.bicep index b404389c..9c484b65 100644 --- a/openapi-to-sdk/infra/resources.bicep +++ b/openapi-to-sdk/infra/resources.bicep @@ -12,7 +12,7 @@ param principalId string var abbrs = loadJsonContent('./abbreviations.json') var resourceToken = uniqueString(subscription().id, resourceGroup().id, location) -// [추가] 1. 스토리지 계정 및 파일 공유 생성 +// Storage account and file share for the workspace resource storage 'Microsoft.Storage/storageAccounts@2025-01-01' = { name: '${abbrs.storageStorageAccounts}${resourceToken}' location: location @@ -85,7 +85,7 @@ module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.4.5 } } -// [추가] 2. 환경과 스토리지 연결 +// Link storage to the container apps environment resource env 'Microsoft.App/managedEnvironments@2025-01-01' existing = { name: '${abbrs.appManagedEnvironments}${resourceToken}' } @@ -137,12 +137,12 @@ module mcpOpenApiToSdk 'br/public:avm/res/app/container-app:0.8.0' = { ] } - // [추가] 3. 볼륨 정의 (위에서 만든 envStorage의 이름을 참조) + // Define the volume using the envStorage created above volumes: [ { - name: 'workspace-vol' // 앱 내부에서 쓸 볼륨 식별자 + name: 'workspace-vol' storageType: 'AzureFile' - storageName: 'workspace' // envStorage 리소스의 name과 일치해야 함 + storageName: 'workspace' } ] @@ -172,7 +172,7 @@ module mcpOpenApiToSdk 'br/public:avm/res/app/container-app:0.8.0' = { '--http' ] - // [추가] 4. 컨테이너 내부 경로에 마운트 + // Mount the volume to a path inside the container volumeMounts: [ { volumeName: 'workspace-vol' diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs index 728fa0bf..16e5742f 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs @@ -6,22 +6,20 @@ using McpSamples.Shared.Configurations; using McpSamples.Shared.Extensions; -// 1. 실행 모드 감지 (Shared 기능 사용) +/// +/// This is the entry point for the OpenAPI to SDK Hybrid App. +/// It configures the application host, registers services, and sets up runtime settings. +/// var useStreamableHttp = AppSettings.UseStreamableHttp(Environment.GetEnvironmentVariables(), args); -// 2. 호스트 빌더 생성 IHostApplicationBuilder builder = useStreamableHttp ? WebApplication.CreateBuilder(args) : Host.CreateApplicationBuilder(args); -// 3. 설정(AppSettings) 등록 (Shared 기능 사용) -// appsettings.json 로드 및 객체 바인딩 처리 builder.Services.AddAppSettings(builder.Configuration, args); -// [추가] HttpContext에 접근하기 위해 등록 (HTTP 모드일 때만 필수지만, 안전하게 항상 등록해도 무방함) builder.Services.AddHttpContextAccessor(); -// 4. 서비스 등록 var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, @@ -31,34 +29,23 @@ }; builder.Services.AddSingleton(options); -// 핵심 비즈니스 로직 서비스 및 프롬프트 서비스 등록 builder.Services.AddSingleton(); builder.Services.AddSingleton(); -// 5. 앱 빌드 (Shared 기능 사용) -// 이 단계에서 [McpServerToolType], [McpServerPromptType] 어트리뷰트가 있는 클래스를 자동으로 스캔하여 등록합니다. IHost app = builder.BuildApp(useStreamableHttp); -// -------------------------------------------------------------------------- -// [Runtime Configuration] 실행 환경(Local/Docker/Azure)에 따른 경로 설정 -// -------------------------------------------------------------------------- var appSettings = app.Services.GetRequiredService(); InitializeRuntimeSettings(appSettings, useStreamableHttp); -// -------------------------------------------------------------------------- -// [HTTP Mode Only] 다운로드를 위한 정적 파일 서빙 설정 -// -------------------------------------------------------------------------- if (useStreamableHttp) { var webApp = (app as WebApplication)!; - // 저장 경로가 없으면 생성 if (!Directory.Exists(appSettings.GeneratedPath)) { Directory.CreateDirectory(appSettings.GeneratedPath); } - // '/download' 경로를 물리적 폴더에 매핑 webApp.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(appSettings.GeneratedPath), @@ -66,17 +53,14 @@ ServeUnknownFileTypes = true }); - // [신규] 파일 업로드 엔드포인트 추가 (/upload) webApp.MapPost("/upload", async (IFormFile file, OpenApiToSdkAppSettings settings) => { if (file == null || file.Length == 0) return Results.BadRequest("No file uploaded."); - // 저장 경로: workspace/specs/{파일명} if (!Directory.Exists(settings.SpecsPath)) Directory.CreateDirectory(settings.SpecsPath); - // 보안상 파일명만 추출 (경로 조작 방지) var fileName = Path.GetFileName(file.FileName); var filePath = Path.Combine(settings.SpecsPath, fileName); @@ -87,20 +71,19 @@ return Results.Ok(new { Message = "File uploaded successfully.", SavedPath = filePath }); }) - .DisableAntiforgery(); // 외부(Agent/Curl)에서 호출하기 쉽도록 CSRF 비활성화 + .DisableAntiforgery(); } -// 6. 앱 실행 await app.RunAsync(); - -// -------------------------------------------------------------------------- -// Helper: 런타임 환경 변수 감지 및 경로 주입 -// -------------------------------------------------------------------------- +/// +/// Initializes runtime settings based on the execution environment (Local/Docker/Azure). +/// +/// The instance. +/// A boolean indicating if the application is running in HTTP mode. void InitializeRuntimeSettings(OpenApiToSdkAppSettings settings, bool isHttp) { - // Docker/Azure 환경 변수 확인 bool isContainer = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true"; string? azureAppName = Environment.GetEnvironmentVariable("CONTAINER_APP_NAME"); bool isAzure = !string.IsNullOrEmpty(azureAppName); @@ -109,24 +92,18 @@ void InitializeRuntimeSettings(OpenApiToSdkAppSettings settings, bool isHttp) if (isContainer) { - // 컨테이너 환경: 항상 /app 사용 baseDirectory = "/app"; } else { - // 로컬 환경: 'openapi-to-sdk' 루트 폴더 찾기 - // 1. 현재 실행 위치(CurrentDirectory)에서 시작 - // 2. 상위로 이동하며 'Dockerfile.openapi-to-sdk' 파일이 있는 곳을 찾음 baseDirectory = TryFindProjectRoot(Directory.GetCurrentDirectory()) ?? Directory.GetCurrentDirectory(); baseDirectory = Path.Combine(baseDirectory, "openapi-to-sdk"); - // (디버깅용 로그: 실행 시 콘솔에 출력됨) Console.WriteLine($"[Init] Local Base Directory resolved to: {baseDirectory}"); } string workspacePath = Path.Combine(baseDirectory, "workspace"); - // 설정 객체에 값 주입 settings.WorkspacePath = workspacePath; settings.GeneratedPath = Path.Combine(workspacePath, "generated"); settings.SpecsPath = Path.Combine(workspacePath, "specs"); @@ -139,18 +116,21 @@ void InitializeRuntimeSettings(OpenApiToSdkAppSettings settings, bool isHttp) if (!Directory.Exists(settings.GeneratedPath)) Directory.CreateDirectory(settings.GeneratedPath); } -// 프로젝트 루트 탐색 헬퍼 메서드 +/// +/// Helper method to find the project root directory by searching for 'Dockerfile.openapi-to-sdk'. +/// +/// The path to start searching from. +/// The full path of the project root if found, otherwise null. string? TryFindProjectRoot(string startPath) { var dir = new DirectoryInfo(startPath); while (dir != null) { - // 이 파일이 있는 곳을 루트(openapi-to-sdk 폴더)로 간주 if (dir.GetFiles("Dockerfile.openapi-to-sdk").Length > 0) { return dir.FullName; } dir = dir.Parent; } - return null; // 못 찾으면 null 반환 (현재 위치 사용) + return null; } \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs index 8a4da1cc..6ad05e3e 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs @@ -6,11 +6,35 @@ namespace McpSamples.OpenApiToSdk.HybridApp.Services; +/// +/// This provides interfaces for the OpenAPI service. +/// +public interface IOpenApiService +{ + /// + /// Generates a client SDK from an OpenAPI specification. + /// + /// The URL or local file path of the OpenAPI specification. + /// The target programming language (e.g., CSharp, Python, Java, TypeScript). + /// The name of the generated client class. + /// The namespace for the generated code. + /// Additional Kiota command line options (e.g., --version). + /// The cancellation token. + /// A message indicating the result of the SDK generation. + Task GenerateSdkAsync(string specSource, string language, string? clientClassName, string? namespaceName, string? additionalOptions, CancellationToken cancellationToken = default); +} + +/// +/// This represents the service for generating client SDKs from OpenAPI specifications. +/// +/// instance. +/// instance. +/// instance. public class OpenApiService(OpenApiToSdkAppSettings settings, IHttpContextAccessor httpContextAccessor, ILogger logger) : IOpenApiService { + /// public async Task GenerateSdkAsync(string specSource, string language, string? clientClassName, string? namespaceName, string? additionalOptions, CancellationToken cancellationToken = default) { - // 0. 기본값 설정 및 옵션 처리 if (string.IsNullOrWhiteSpace(specSource)) throw new ArgumentException("Spec source cannot be empty.", nameof(specSource)); var finalClassName = string.IsNullOrWhiteSpace(clientClassName) ? "ApiClient" : clientClassName; @@ -19,7 +43,6 @@ public async Task GenerateSdkAsync(string specSource, string language, s if (finalOptions.Contains("-o ") || finalOptions.Contains("--output ")) { - // -o 옵션 또는 --output 옵션이 포함된 경우 에러로 반환하여 에이전트에게 "옵션 빼고 다시 요청해"라고 가르침 (방어 코드) return """ Input Error: Invalid Option Detected @@ -30,7 +53,6 @@ The output path is managed automatically by the server. """; } - // 1. 입력 소스 판별 (URL vs 파일 경로) string inputPath; bool isUrl = Uri.TryCreate(specSource, UriKind.Absolute, out var uriResult) && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); @@ -44,8 +66,6 @@ The output path is managed automatically by the server. { if (settings.IsContainer || settings.IsAzure) { - // 컨테이너/Azure 환경에서는 마운트된 볼륨의 specs 폴더를 확인합니다. - // 리눅스 컨테이너에서 윈도우 경로(\)가 들어올 경우를 대비해 파일명만 확실하게 추출합니다. string fileName = Path.GetFileName(specSource); if (fileName.Contains('\\')) { @@ -54,21 +74,17 @@ The output path is managed automatically by the server. inputPath = Path.Combine(settings.SpecsPath, fileName); - // 2. 파일이 마운트된 경로에 없으면 에이전트에게 복사를 요청합니다. (에러 처리 X) if (!File.Exists(inputPath)) { - if (settings.IsAzure) // [수정] Azure 환경 처리: HTTP 업로드 안내 + if (settings.IsAzure) { - // 현재 서버의 업로드 URL 계산 - string uploadUrl = "/upload"; // Fallback + string uploadUrl = "/upload"; var request = httpContextAccessor.HttpContext?.Request; if (request != null) { - // 예: https://myapp.azurecontainerapps.io/upload uploadUrl = $"{request.Scheme}://{request.Host}/upload"; } - // Agent에게 'curl' 명령어를 실행하라고 지시 return $""" Action Required: File Upload Needed (Azure) @@ -90,24 +106,17 @@ I am running in an Azure environment and cannot access your local file `{specSou } if (settings.IsContainer) { - // 기본 안내 경로 (환경변수 누락 시 대비) string targetHostPath = $"workspace/specs/{fileName}"; - // 도커 실행 시 주입된 HOST_ROOT_PATH 환경 변수 확인 - // 옵션: -e HOST_ROOT_PATH=${workspaceFolder}/openapi-to-sdk string? hostRootPath = Environment.GetEnvironmentVariable("HOST_ROOT_PATH"); if (!string.IsNullOrEmpty(hostRootPath)) { - // 1. 경로 정규화: 윈도우(\)와 리눅스(/) 구분자를 모두 포워드 슬래시('/')로 통일 string normalizedHostRoot = hostRootPath.Replace('\\', '/').TrimEnd('/'); - // 2. 최종 호스트 타겟 경로 조립 - // 예: D:/Projects/openapi-to-sdk/workspace/specs/petstore.json targetHostPath = $"{normalizedHostRoot}/workspace/specs/{fileName}"; } - // 에러가 아닌 '조치 요청' 메시지 반환 return $""" Action Required: File Synchronization Needed @@ -134,15 +143,12 @@ 2. RETRY the `generate_sdk` tool immediately with the same arguments. logger.LogInformation("Input is a File: {InputPath}", inputPath); } - // 2. 임시 출력 폴더 생성 string outputId = Guid.NewGuid().ToString(); string tempOutputPath = Path.Combine(settings.GeneratedPath, outputId); Directory.CreateDirectory(tempOutputPath); try { - // 3. Kiota 실행 - // additionalOptions가 포함된 Arguments 구성 logger.LogInformation("Starting Kiota generation..."); var startInfo = new ProcessStartInfo @@ -166,14 +172,12 @@ 2. RETRY the `generate_sdk` tool immediately with the same arguments. return $"[Error] Kiota generation failed:\n{error}"; } - // 4. Zip 압축 string zipFileName = $"sdk-{language}-{outputId.Substring(0, 4)}.zip"; string zipFilePath = Path.Combine(settings.GeneratedPath, zipFileName); ZipFile.CreateFromDirectory(tempOutputPath, zipFilePath); logger.LogInformation("SDK generated and zipped at: {ZipFilePath}", zipFilePath); - // 5. 결과 반환 메시지 생성 return CreateResultMessage(zipFileName, zipFilePath); } catch (Exception ex) @@ -197,6 +201,12 @@ 2. RETRY the `generate_sdk` tool immediately with the same arguments. } } + /// + /// Creates a result message for the SDK generation, including download links for HTTP mode. + /// + /// The name of the generated ZIP file. + /// The local path to the generated ZIP file. + /// A formatted string message with download information. private string CreateResultMessage(string zipFileName, string localZipPath) { if (settings.IsHttpMode) @@ -204,20 +214,15 @@ private string CreateResultMessage(string zipFileName, string localZipPath) string relativePath = $"/download/{zipFileName}"; string downloadUrl; - // 현재 요청(Request) 정보 가져오기 var request = httpContextAccessor.HttpContext?.Request; if (request != null) { - // Local, Docker, Azure 모두 현재 접속된 Host(도메인+포트)를 기준으로 URL 생성 - // 예: http://localhost:5222, https://myapp.azurecontainerapps.io 등 string baseUrl = $"{request.Scheme}://{request.Host}"; downloadUrl = $"{baseUrl}{relativePath}"; } else { - // HttpContext가 없는 예외적인 경우 (Fallback) - // Azure나 Docker는 보통 포트 8080, 로컬은 5222 등 다양하므로 상대 경로만 제공 downloadUrl = relativePath; } @@ -228,27 +233,19 @@ private string CreateResultMessage(string zipFileName, string localZipPath) { string finalPath = localZipPath; - if (settings.IsContainer) // Docker Stdio 모드 + if (settings.IsContainer) { - // 호스트 경로 환경 변수 읽기 - // 예: "D:/KNU/3-2/CDP1/openapi-to-sdk" string? hostRootPath = Environment.GetEnvironmentVariable("HOST_ROOT_PATH"); if (!string.IsNullOrEmpty(hostRootPath)) { - // 1. 컨테이너 경로의 시작인 /app을 제외합니다. - // finalPath = /app/workspace/... string relativePathFromApp = finalPath.Substring("/app".Length).TrimStart('/'); - // 2. 호스트 경로의 끝에 있는 슬래시(/)나 역슬래시(\)를 정리합니다. string hostPathNormalized = hostRootPath.TrimEnd('/', '\\'); - // 3. 크로스 플랫폼 호환성을 위해 최종 경로를 포워드 슬래시(/)로 연결합니다. - // Path.Combine 대신 string concatenation을 사용하여 OS 종속성을 제거합니다. finalPath = $"{hostPathNormalized}/{relativePathFromApp}"; } } - // Stdio 모드 return $"SDK Generation Successful!\n\n" + $"File Saved At: {localZipPath}\n\n" + $"The file is currently in the workspace. Please check if this location is correct.\n" + diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs index 61340186..9918bdc5 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Tools/OpenApiToSdkTool.cs @@ -46,7 +46,6 @@ public async Task GenerateSdkAsync( [Description("Additional Kiota command line options (e.g., --version).")] string? additionalOptions = null) { - // Service 호출 var resultMessage = await service.GenerateSdkAsync( specSource, language, From 880d4e601ee21a4469e413d8b08faa6a4eccdade Mon Sep 17 00:00:00 2001 From: x-or-b Date: Thu, 4 Dec 2025 21:21:16 +0900 Subject: [PATCH 49/61] Refactor SDK generation success message formatting for improved readability --- .../Services/OpenApiService.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs index 6ad05e3e..84eef471 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs @@ -94,11 +94,9 @@ I am running in an Azure environment and cannot access your local file `{specSou Please execute the appropriate command for your operating system (OS) using your terminal or shell tool: ### macOS & Linux - curl -F "file=@{specSource}" {uploadUrl} ### Windows (PowerShell/CMD) - curl.exe -F "file=@{specSource}" {uploadUrl} After the command completes successfully, please call this `generate_sdk` tool again. @@ -246,8 +244,8 @@ private string CreateResultMessage(string zipFileName, string localZipPath) finalPath = $"{hostPathNormalized}/{relativePathFromApp}"; } } - return $"SDK Generation Successful!\n\n" + - $"File Saved At: {localZipPath}\n\n" + + return $"SDK Generation Successful!\n" + + $"File Saved At: {localZipPath}\n" + $"The file is currently in the workspace. Please check if this location is correct.\n" + $"If the user wants the file elsewhere, please move it to the desired destination."; } From 95878a4beeb0c0d89b47e4d1b6d3f3b44a51c160 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Thu, 4 Dec 2025 21:25:22 +0900 Subject: [PATCH 50/61] Refactor SDK generation success message formatting for consistency and clarity --- .../Prompts/SdkGenerationPrompt.cs | 6 +----- .../Services/OpenApiService.cs | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs index bdf4b5d4..c35537d2 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Prompts/SdkGenerationPrompt.cs @@ -31,16 +31,12 @@ public class SdkGenerationPrompt : ISdkGenerationPrompt public string GetSdkGenerationPrompt( [Description("The URL or local file path of the OpenAPI specification.")] string specSource, - [Description("The target programming language. Supported values: CSharp, Java, TypeScript, PHP, Python, Go, Ruby, Dart, HTTP.")] string language, - [Description("The name of the generated client class. Default: 'ApiClient'.")] string? clientClassName = "ApiClient", - [Description("The namespace for the generated code. Default: 'ApiSdk'.")] string? namespaceName = "ApiSdk", - [Description("Any additional options for Kiota generation (e.g., --version).")] string? additionalOptions = "None") { @@ -79,7 +75,7 @@ You MUST intelligently map the user's input to one of these valid identifiers. 2. **Handle Output Path**: - The `generate_sdk` tool manages the output path internally to create a ZIP file. - - **NEVER** pass `-o` or `--output` in the `additionalOptions` argument, even if the user asks to save it to a specific location (e.g., "Generate to D:/Work"). + - NEVER pass `-o` or `--output` in the `additionalOptions` argument, even if the user asks to save it to a specific location (e.g., "Generate to D:/Work"). - Instead, follow this workflow: 1. Call `generate_sdk` WITHOUT the output path option. 2. Once the tool returns the ZIP file path (or download link), tell the user: "I have generated the SDK. Would you like me to move/extract it to [User's Requested Path]?" diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs index 84eef471..d34528ea 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs @@ -224,7 +224,7 @@ private string CreateResultMessage(string zipFileName, string localZipPath) downloadUrl = relativePath; } - return $"SDK Generation Successful!\n\n" + + return $"SDK Generation Successful!\n" + $"Download Link: {downloadUrl}"; } else From e52c502edf4fc25e8816ad75bbe0d9d88b10b08e Mon Sep 17 00:00:00 2001 From: x-or-b Date: Thu, 4 Dec 2025 21:32:47 +0900 Subject: [PATCH 51/61] Update README.md to correct prompt name and enhance SDK generation instructions for clarity --- openapi-to-sdk/README.md | 91 ++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/openapi-to-sdk/README.md b/openapi-to-sdk/README.md index 8478c8ca..b837a37c 100644 --- a/openapi-to-sdk/README.md +++ b/openapi-to-sdk/README.md @@ -23,7 +23,7 @@ OpenAPI to SDK MCP server includes: | Building Block | Name | Description | Usage | |----------------|----------------|---------------------------------------------------------------------------------------------------------|-----------------------| | Tools | `generate_sdk` | Generates a client SDK from an OpenAPI specification (URL or raw content) and returns a download link. | `#generate_sdk` | -| Prompts | `generate_sdk` | A structured prompt that guides the LLM to generate an SDK, handling language normalization and inputs. | `/mcp.openapi-to-sdk.generate_sdk` | +| Prompts | `generate_sdk_prompt` | A structured prompt that guides the LLM to generate an SDK, handling language normalization and inputs. | `/mcp.openapi-to-sdk.generate_sdk_prompt` | ## Getting Started @@ -222,50 +222,51 @@ OpenAPI to SDK MCP server includes: 1. Use a prompt by typing `/mcp.openapi-to-sdk.generate_sdk` and enter keywords to search. You'll get a prompt like: ```text - You are an expert SDK generator using Microsoft Kiota. - - 1. User Input Analysis - - OpenAPI Source: "{openApiSource}" - - Target Language: "{language}" - - Configuration: - - Class Name: {className ?? "Default (ApiClient)"} - - Namespace: {namespaceName ?? "Default (ApiSdk)"} - - Options: {additionalOptions ?? "None"} - - --- - 2. Execution Strategy (Follow Strictly) - - Step 1: Validate & Normalize Language - Match the input to a valid Kiota identifier: [ CSharp, Go, Java, PHP, Python, Ruby, Shell, Swift, TypeScript ]. - - If a match or alias is found (e.g., "ts" -> "TypeScript", "golang" -> "Go"), use the valid identifier. - - If NO match is found (e.g., "Rust", "C++", "asdf"), STOP immediately and ask the user to provide a supported language. - - Step 2: Resolve OpenAPI Source (CRITICAL) - The 'generate_sdk' tool accepts either a URL or Raw Content, but NOT a file path. - Analyze the [OpenAPI Source] provided above: - - - CASE A: It is a URL (starts with http/https) - - Action: Pass the URL string directly to the `specSource` argument. - - - CASE B: It looks like a File Path (e.g., "C:\specs\api.json", "./swagger.yaml") - - Action: You MUST first read the content of this file using your available tools (e.g., `filesystem` tool). - - Then, pass the file content (JSON/YAML text) to the `specSource` argument. - - If you cannot read the file, ask the user to paste the content directly. - - - CASE C: It is Raw JSON/YAML Content - - Action: Pass the content string directly to the `specSource` argument. - - Step 3: Call Tool - Call the `generate_sdk` tool with the prepared arguments: - - `language`: (The normalized identifier from Step 1) - - `specSource`: (The resolved URL or Content from Step 2) - - `className`: (As provided) - - `namespaceName`: (As provided) - - `additionalOptions`: (As provided) - - Step 4: Report Results - - If a download link is returned, display it clearly. - - If a local path is returned, provide the path. + You are an expert SDK generator using Microsoft Kiota. + + Your task is to generate a client SDK based on the following inputs: + - OpenAPI Source: `{specSource}` + - Target Language: `{language}` + - Configuration: + - Class Name: {clientClassName} + - Namespace: {namespaceName} + - Additional Options: {additionalOptions} + + --- + ### Execution Rules (Follow Strictly) + + 1. **Smart Language Normalization**: + The `generate_sdk` tool ONLY accepts the following language identifiers: + [ CSharp, Java, TypeScript, PHP, Python, Go, Ruby, Dart, HTTP ] + + You MUST intelligently map the user's input to one of these valid identifiers. + + - **Handle Aliases & Variations**: + - "C#", "c#", ".NET", "dotnet", "chsarp" (typo) -> Use CSharp + - "TS", "Ts", "ts", "node", "typoscript" (typo) -> Use TypeScript + - "Golang", "Goo" (typo) -> Use Go + - "py", "pyton" (typo), "python3" -> Use Python + - "jav", "Jave" (typo) -> Use Java + + - **Auto-Correction**: + - If the user makes a minor typo or uses a common abbreviation, automatically correct it to the nearest valid identifier from the list above. + + - **Validation**: + - If the input refers to a completely unsupported language (e.g., "Rust", "C++", "Assembly"), STOP and politely inform the user that it is not currently supported by Kiota. + + 2. **Handle Output Path**: + - The `generate_sdk` tool manages the output path internally to create a ZIP file. + - NEVER pass `-o` or `--output` in the `additionalOptions` argument, even if the user asks to save it to a specific location (e.g., "Generate to D:/Work"). + - Instead, follow this workflow: + 1. Call `generate_sdk` WITHOUT the output path option. + 2. Once the tool returns the ZIP file path (or download link), tell the user: "I have generated the SDK. Would you like me to move/extract it to [User's Requested Path]?" + 3. If the user agrees, use your filesystem tools to move the file. + + 3. **Call the Tool**: + Use the `generate_sdk` tool with the normalized language and filtered options (excluding -o). + + 4. **Report**: + Provide the download link or file path returned by the tool. ``` 1. Confirm the result. \ No newline at end of file From 104c074fde7866774196dc921991fbdedbf1bd84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=A5=ED=83=9C=EA=B7=9C?= <114312575+x-or-b@users.noreply.github.com> Date: Thu, 4 Dec 2025 21:42:57 +0900 Subject: [PATCH 52/61] Create wwwroot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 작동을 위하여 wwwroot 폴더를 추가하였습니다. --- openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/wwwroot | 1 + 1 file changed, 1 insertion(+) create mode 100644 openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/wwwroot diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/wwwroot b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/wwwroot new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/wwwroot @@ -0,0 +1 @@ + From e889409e91b0f2d9d3e6fc7dff18f4f48fd7a008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=A5=ED=83=9C=EA=B7=9C?= <114312575+x-or-b@users.noreply.github.com> Date: Thu, 4 Dec 2025 21:44:26 +0900 Subject: [PATCH 53/61] Delete openapi-to-sdk/plan.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 불필요한 파일을 삭제하였습니다. --- openapi-to-sdk/plan.md | 83 ------------------------------------------ 1 file changed, 83 deletions(-) delete mode 100644 openapi-to-sdk/plan.md diff --git a/openapi-to-sdk/plan.md b/openapi-to-sdk/plan.md deleted file mode 100644 index 99905123..00000000 --- a/openapi-to-sdk/plan.md +++ /dev/null @@ -1,83 +0,0 @@ -### Epic: OpenAPI 기반 SDK 자동 생성 (MCP Server) - -- 사용자가 LLM 클라이언트를 통해 OpenAPI 명세를 제공하면, 원하는 언어의 SDK를 생성하고 받을 수 있는 MCP 서버 환경을 구축한다. - ---- - -### Feature 1: SDK 생성 및 반환 - -- **설명**: OpenAPI 명세를 받아 Kiota를 실행하고 결과를 압축한다. - -- User Story 1.1: 시스템으로서, 나는 OpenAPI 명세를 받아 SDK를 생성하고 압축하고 싶다. - - - **Tasks**: - - [ ] OpenAPI URL에서 명세 콘텐츠를 다운로드하거나, 전달받은 콘텐츠를 처리하는 모듈 구현. - - [ ] Kiota CLI 명령으로 옵션을 매핑하는 로직 구현. - - [ ] Process 클래스를 이용한 Kiota CLI 실행 래퍼 구현. - - [ ] 임시 폴더에 Kiota를 실행하여 SDK 소스 코드 생성. - - [ ] 생성된 SDK 폴더 전체를 하나의 ZIP 파일로 압축하는 로직 구현. - -- User Story 1.2: LLM 클라이언트로서, 나는 SDK 생성 결과를 받고 싶다. - - **Tasks**: - - [ ] LLM 클라이언트가 접근 가능한 URI ZIP 파일의 경로를 반환한다. - ---- - -### Feature 2: 오류 처리 및 피드백 - -- **설명**: SDK 생성 전 과정에서 발생하는 오류를 감지하고, 이를 구조화된 형태로 사용자에게 전달한다. - -- User Story 2.1: LLM 클라이언트로서, 나는 SDK 생성 실패 오류 메시지를 받고 싶다. - - - **Tasks**: - - [ ] 표준 오류 메시지의 JSON 구조 정의 (`{"errorCode": "string", "message": ...}`). - - [ ] 발생한 예외를 표준 오류 메시지로 변환 후, 적절한 응답 본문으로 반환. - -- User Story 2.2: 시스템으로서, 나는 Kiota 실행 오류를 구조화하여 처리하고 싶다. - - **Tasks**: - - [ ] Kiota CLI 실행 및 프로세스 관리를 위한 서비스 구현. - - [ ] Kiota 실행 결과 파싱 및 오류 매핑 로직 구현. - - [ ] 비동기 Kiota 실행 및 타임아웃 처리 구현. - ---- - -### Feature 3: LLM 연동 및 실행 지원 - -- **설명**: LLM 클라이언트가 서버의 기능을 쉽게 사용하도록 돕고, 다양한 환경에 MCP 서버를 실행할 수 있게 한다. - -- User Story 3.1: LLM 클라이언트로서, 나는 SDK 생성 기능을 쉽게 사용할 수 있도록 Pre-defined Prompt를 받고 싶다. - - - **Tasks**: - - [ ] SDK 생성 요청(Tools)을 위한 Pre-defined Prompt 정의. - -- User Story 3.2: 시스템으로서, 나는 MCP 서버를 초기화하고 서비스를 등록하고 싶다. - - **Tasks**: - - [ ] Program.cs에서 하이브리드 MCP 서버 초기화 구현 (STDIO/HTTP 모드 지원). - - [ ] OpenApiToSdkAppSettings 구성 및 DI 컨테이너 등록. - - [ ] Kiota 실행을 위한 서비스 등록 (HttpClient, 파일 처리 서비스). - - [ ] 프롬프트 및 도구 자동 등록을 위한 어셈블리 스캔 설정. - ---- - -### Feature 4: 인프라 및 배포 지원 - -- **설명**: 다양한 환경에서 서버를 실행하고 배포할 수 있도록 지원한다. - -- User Story 4.1: 개발자로서, 나는 로컬 개발 환경에서 서버를 실행하고 싶다. - - - **Tasks**: - - [ ] .vscode/mcp.stdio.local.json 및 mcp.http.local.json 설정 파일 작성. - - [ ] launchSettings.json에서 개발 환경 프로필 구성. - - [ ] appsettings.Development.json 작성. - -- User Story 4.2: 운영자로서, 나는 컨테이너 환경에서 서버를 배포하고 싶다. - - - **Tasks**: - - [ ] .vscode/mcp.stdio.container.json 및 mcp.http.container.json 설정 파일 작성. - - [ ] Dockerfile.openapi-to-sdk 작성. - -- User Story 4.3: 운영자로서, 나는 Azure 환경에서 서버를 배포하고 싶다. - - **Tasks**: - - [ ] .vscode/mcp.http.remote.json 설정 파일 작성. - - [ ] Azure Bicep 템플릿 (main.bicep, resources.bicep) 작성. - - [ ] azure.yaml 및 배포 스크립트 작성. From f4e6d025f677a9468805ad18c63714155c3a08cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=A5=ED=83=9C=EA=B7=9C?= <114312575+x-or-b@users.noreply.github.com> Date: Thu, 4 Dec 2025 21:54:17 +0900 Subject: [PATCH 54/61] Delete openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/wwwroot --- openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/wwwroot | 1 - 1 file changed, 1 deletion(-) delete mode 100644 openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/wwwroot diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/wwwroot b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/wwwroot deleted file mode 100644 index 8b137891..00000000 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/wwwroot +++ /dev/null @@ -1 +0,0 @@ - From b00abf44649eaab0a72ddf00ed93037a9a8b8cc9 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Thu, 4 Dec 2025 21:56:17 +0900 Subject: [PATCH 55/61] Add .gitkeep to wwwroot directory --- .../src/McpSamples.OpenApiToSdk.HybridApp/wwwroot/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/wwwroot/.gitkeep diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/wwwroot/.gitkeep b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/wwwroot/.gitkeep new file mode 100644 index 00000000..e69de29b From 7cf7a11a1722a7093356c2c87d7efc95b19d29cb Mon Sep 17 00:00:00 2001 From: x-or-b Date: Thu, 4 Dec 2025 21:59:09 +0900 Subject: [PATCH 56/61] Remove IOpenApiService interface and its documentation from OpenApiService.cs --- .../Services/OpenApiService.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs index d34528ea..3d017170 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs @@ -6,24 +6,6 @@ namespace McpSamples.OpenApiToSdk.HybridApp.Services; -/// -/// This provides interfaces for the OpenAPI service. -/// -public interface IOpenApiService -{ - /// - /// Generates a client SDK from an OpenAPI specification. - /// - /// The URL or local file path of the OpenAPI specification. - /// The target programming language (e.g., CSharp, Python, Java, TypeScript). - /// The name of the generated client class. - /// The namespace for the generated code. - /// Additional Kiota command line options (e.g., --version). - /// The cancellation token. - /// A message indicating the result of the SDK generation. - Task GenerateSdkAsync(string specSource, string language, string? clientClassName, string? namespaceName, string? additionalOptions, CancellationToken cancellationToken = default); -} - /// /// This represents the service for generating client SDKs from OpenAPI specifications. /// From e2ddd69a13817e7e358c72072dd1d6ab2205c633 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Sat, 6 Dec 2025 23:32:09 +0900 Subject: [PATCH 57/61] Refactor OpenAPI to SDK configuration and update Dockerfiles for improved structure and clarity --- .vscode/mcp.json | 14 ++------------ Dockerfile.openapi-to-sdk | 19 ------------------- Dockerfile.openapi-to-sdk-azure | 10 ++++++---- .../.vscode/mcp.stdio.container.json | 11 +++++++++-- openapi-to-sdk/README.md | 11 ++++++----- openapi-to-sdk/workspace/generated/.gitkeep | 0 openapi-to-sdk/workspace/specs/.gitkeep | 0 7 files changed, 23 insertions(+), 42 deletions(-) create mode 100644 openapi-to-sdk/workspace/generated/.gitkeep create mode 100644 openapi-to-sdk/workspace/specs/.gitkeep diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 09cf41d2..c52e811a 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,18 +1,8 @@ { "servers": { "openapi-to-sdk": { - "type": "stdio", - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-v", - "${workspaceFolder}/openapi-to-sdk/workspace:/app/workspace", - "-e", - "HOST_ROOT_PATH=${workspaceFolder}/openapi-to-sdk", - "openapi-to-sdk:latest" - ] + "type": "http", + "url": "http://localhost:8080/mcp" } } } \ No newline at end of file diff --git a/Dockerfile.openapi-to-sdk b/Dockerfile.openapi-to-sdk index fd403246..a817390b 100644 --- a/Dockerfile.openapi-to-sdk +++ b/Dockerfile.openapi-to-sdk @@ -1,17 +1,12 @@ # syntax=docker/dockerfile:1 -# ----------------------------------------------------------------------------- -# Build Stage -# ----------------------------------------------------------------------------- FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build -# 소스 코드 복사 (Shared 프로젝트 포함) COPY ./shared/McpSamples.Shared /source/shared/McpSamples.Shared COPY ./openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp /source/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp WORKDIR /source/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp -# Kiota 도구 설치 (빌드 스테이지에서 설치 후 복사) RUN dotnet tool install --global Microsoft.OpenApi.Kiota # 아키텍처에 맞게 게시 (Publish) @@ -23,37 +18,23 @@ RUN case "$TARGETARCH" in \ esac && \ dotnet publish -c Release -o /app -r $RID --self-contained false -# ----------------------------------------------------------------------------- -# Runtime Stage -# ----------------------------------------------------------------------------- FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final WORKDIR /app -# 1. 빌드 결과물 복사 COPY --from=build /app . -# 2. Kiota 도구 복사 및 설정 COPY --from=build /root/.dotnet/tools /opt/kiota-tools -# [중요] 권한 설정 및 심볼릭 링크 -# - /opt/kiota-tools: 실행 가능하도록 설정 -# - kiota: 전역 경로에 심볼릭 링크 연결 RUN chmod -R 755 /opt/kiota-tools && \ ln -s /opt/kiota-tools/kiota /usr/local/bin/kiota -# 3. [개선] Workspace 디렉터리 생성 및 권한 부여 -# - 미리 폴더를 만들어두지 않으면, 앱 실행 시 권한 오류가 날 수 있습니다. -# - $APP_UID는 .NET 이미지에 내장된 변수입니다. RUN mkdir -p /app/workspace/generated && \ mkdir -p /app/workspace/specs && \ chown -R $APP_UID:$APP_UID /app/workspace -# PATH 환경 변수 설정 ENV PATH="/opt/kiota-tools:${PATH}" -# 4. 보안을 위해 비-루트 사용자 전환 USER $APP_UID -# 실행 ENTRYPOINT ["dotnet", "McpSamples.OpenApiToSdk.HybridApp.dll"] \ No newline at end of file diff --git a/Dockerfile.openapi-to-sdk-azure b/Dockerfile.openapi-to-sdk-azure index 3f953fb5..9d817b7c 100644 --- a/Dockerfile.openapi-to-sdk-azure +++ b/Dockerfile.openapi-to-sdk-azure @@ -2,7 +2,6 @@ FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build -# Install Kiota CLI RUN dotnet tool install --global Microsoft.OpenApi.Kiota COPY ./shared/McpSamples.Shared /source/shared/McpSamples.Shared @@ -19,10 +18,13 @@ WORKDIR /app COPY --from=build /app . COPY --from=build /root/.dotnet/tools /opt/kiota-tools -RUN mkdir -p /app/wwwroot/generated && \ - chown -R $APP_UID:$APP_UID /app/wwwroot +RUN mkdir -p /app/workspace/generated && \ + mkdir -p /app/workspace/specs && \ + chown -R $APP_UID:$APP_UID /app/workspace && \ + chmod -R 755 /opt/kiota-tools && \ + ln -s /opt/kiota-tools/kiota /usr/local/bin/kiota -RUN ln -s /opt/kiota-tools/kiota /usr/local/bin/kiota +ENV PATH="/opt/kiota-tools:${PATH}" USER $APP_UID diff --git a/openapi-to-sdk/.vscode/mcp.stdio.container.json b/openapi-to-sdk/.vscode/mcp.stdio.container.json index 09cf41d2..82d4e56c 100644 --- a/openapi-to-sdk/.vscode/mcp.stdio.container.json +++ b/openapi-to-sdk/.vscode/mcp.stdio.container.json @@ -1,4 +1,11 @@ { + "inputs": [ + { + "type": "promptString", + "id": "repository-root", + "description": "Enter the absolute path to the repository root directory (where 'openapi-to-sdk' folder is located)." + } + ], "servers": { "openapi-to-sdk": { "type": "stdio", @@ -8,9 +15,9 @@ "-i", "--rm", "-v", - "${workspaceFolder}/openapi-to-sdk/workspace:/app/workspace", + "${input:repository-root}/workspace:/app/workspace", "-e", - "HOST_ROOT_PATH=${workspaceFolder}/openapi-to-sdk", + "HOST_ROOT_PATH=${input:repository-root}", "openapi-to-sdk:latest" ] } diff --git a/openapi-to-sdk/README.md b/openapi-to-sdk/README.md index b837a37c..3367d6f6 100644 --- a/openapi-to-sdk/README.md +++ b/openapi-to-sdk/README.md @@ -82,13 +82,13 @@ OpenAPI to SDK MCP server includes: 1. Run the MCP server app in a container. ```bash - docker run -i --rm -p 8080:8080 -v "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace" -e HOST_ROOT_PATH="$REPOSITORY_ROOT" openapi-to-sdk:latest + docker run -i --rm -p 8080:8080 -v "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace" -e HOST_ROOT_PATH="$REPOSITORY_ROOT/openapi-to-sdk" openapi-to-sdk:latest ``` Alternatively, use the container image from the container registry. ```bash - docker run -i --rm -p 8080:8080 -v "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace" -e HOST_ROOT_PATH="$REPOSITORY_ROOT" ghcr.io/microsoft/mcp-dotnet-samples/openapi-to-sdk:latest + docker run -i --rm -p 8080:8080 -v "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace" -e HOST_ROOT_PATH="$REPOSITORY_ROOT/openapi-to-sdk" ghcr.io/microsoft/mcp-dotnet-samples/openapi-to-sdk:latest ``` **Parameters**: @@ -99,12 +99,12 @@ OpenAPI to SDK MCP server includes: ```bash # use local container image - docker run -i --rm -p 8080:8080 -v "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace" -e HOST_ROOT_PATH="$REPOSITORY_ROOT" openapi-to-sdk:latest --http + docker run -i --rm -p 8080:8080 -v "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace" -e HOST_ROOT_PATH="$REPOSITORY_ROOT/openapi-to-sdk" openapi-to-sdk:latest --http ``` ```bash # use container image from the container registry - docker run -i --rm -p 8080:8080 -v "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace" -e HOST_ROOT_PATH="$REPOSITORY_ROOT" ghcr.io/microsoft/mcp-dotnet-samples/openapi-to-sdk:latest --http + docker run -i --rm -p 8080:8080 -v "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace" -e HOST_ROOT_PATH="$REPOSITORY_ROOT/openapi-to-sdk" ghcr.io/microsoft/mcp-dotnet-samples/openapi-to-sdk:latest --http ``` #### On Azure @@ -217,7 +217,8 @@ OpenAPI to SDK MCP server includes: 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 `openapi-to-sdk` then click `Start Server`. 1. When prompted, enter one of the following values: - - The absolute directory path of the `McpSamples.OpenApiToSdk.HybridApp` project + - The absolute directory path of the `McpSamples.OpenApiToSdk.HybridApp` project (On a local machine) + - The absolute directory path of the `openapi-to-sdk` project (In a container) - The FQDN of Azure Container Apps. 1. Use a prompt by typing `/mcp.openapi-to-sdk.generate_sdk` and enter keywords to search. You'll get a prompt like: diff --git a/openapi-to-sdk/workspace/generated/.gitkeep b/openapi-to-sdk/workspace/generated/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/openapi-to-sdk/workspace/specs/.gitkeep b/openapi-to-sdk/workspace/specs/.gitkeep new file mode 100644 index 00000000..e69de29b From d78cc5497c335e32b80886f377fdb9a2a07a1f6d Mon Sep 17 00:00:00 2001 From: x-or-b Date: Sat, 6 Dec 2025 23:32:43 +0900 Subject: [PATCH 58/61] Remove commented architecture publish instruction from Dockerfile --- Dockerfile.openapi-to-sdk | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile.openapi-to-sdk b/Dockerfile.openapi-to-sdk index a817390b..f9014497 100644 --- a/Dockerfile.openapi-to-sdk +++ b/Dockerfile.openapi-to-sdk @@ -9,7 +9,6 @@ WORKDIR /source/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp RUN dotnet tool install --global Microsoft.OpenApi.Kiota -# 아키텍처에 맞게 게시 (Publish) ARG TARGETARCH RUN case "$TARGETARCH" in \ "amd64") RID="linux-musl-x64" ;; \ From f658a370eb21a298ed40dd17f5d97ba04a7f52cc Mon Sep 17 00:00:00 2001 From: x-or-b Date: Sun, 7 Dec 2025 23:43:14 +0900 Subject: [PATCH 59/61] Update MCP configuration and improve Azure file upload instructions --- .vscode/mcp.json | 28 +++++++++++++++++-- openapi-to-sdk/README.md | 11 +++++--- .../Services/OpenApiService.cs | 10 +++---- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/.vscode/mcp.json b/.vscode/mcp.json index c52e811a..3bd62f4f 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,8 +1,32 @@ { "servers": { - "openapi-to-sdk": { + "awesome-copilot": { + "type": "stdio", + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "ghcr.io/microsoft/mcp-dotnet-samples/awesome-copilot:latest" + ] + }, + "microsoft.docs.mcp": { "type": "http", - "url": "http://localhost:8080/mcp" + "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" + ] } } } \ No newline at end of file diff --git a/openapi-to-sdk/README.md b/openapi-to-sdk/README.md index 3367d6f6..0754316c 100644 --- a/openapi-to-sdk/README.md +++ b/openapi-to-sdk/README.md @@ -69,6 +69,7 @@ OpenAPI to SDK MCP server includes: ```bash dotnet run --project ./src/McpSamples.OpenApiToSdk.HybridApp -- --http + ``` #### In a container @@ -79,17 +80,19 @@ OpenAPI to SDK MCP server includes: docker build -f Dockerfile.openapi-to-sdk -t openapi-to-sdk:latest . ``` + > Make sure take note the absolute directory path of the `openapi-to-sdk` project. + 1. Run the MCP server app in a container. ```bash docker run -i --rm -p 8080:8080 -v "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace" -e HOST_ROOT_PATH="$REPOSITORY_ROOT/openapi-to-sdk" openapi-to-sdk:latest ``` - Alternatively, use the container image from the container registry. + **Parameters**: @@ -102,10 +105,10 @@ OpenAPI to SDK MCP server includes: docker run -i --rm -p 8080:8080 -v "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace" -e HOST_ROOT_PATH="$REPOSITORY_ROOT/openapi-to-sdk" openapi-to-sdk:latest --http ``` - ```bash + #### On Azure diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs index 3d017170..e598905c 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs @@ -67,19 +67,19 @@ The output path is managed automatically by the server. uploadUrl = $"{request.Scheme}://{request.Host}/upload"; } - return $""" + return $$""" Action Required: File Upload Needed (Azure) - I am running in an Azure environment and cannot access your local file `{specSource}` directly. + I am running in an Azure environment and cannot access your local file `{{specSource}}` 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 - curl -F "file=@{specSource}" {uploadUrl} + curl -F "file=@{{specSource}}" {{uploadUrl}} - ### Windows (PowerShell/CMD) - curl.exe -F "file=@{specSource}" {uploadUrl} + ### Windows (PowerShell) + Invoke-RestMethod -Uri "{{uploadUrl}}" -Method Post -Form @{file = Get-Item -Path "{{specSource}}"} After the command completes successfully, please call this `generate_sdk` tool again. """; From 0314d52fe40ff44297a0f64aefa71a1d985a7428 Mon Sep 17 00:00:00 2001 From: x-or-b Date: Sat, 13 Dec 2025 02:50:52 +0900 Subject: [PATCH 60/61] Refactor runtime settings by using tags and update container configurations for Azure and local modes. --- .../.vscode/mcp.stdio.container.json | 11 +- openapi-to-sdk/infra/resources.bicep | 3 +- .../Configurations/OpenApiToSdkAppSettings.cs | 51 ++++- .../Program.cs | 35 +--- .../Services/OpenApiService.cs | 191 ++++++++---------- .../appsettings.json | 5 +- 6 files changed, 143 insertions(+), 153 deletions(-) diff --git a/openapi-to-sdk/.vscode/mcp.stdio.container.json b/openapi-to-sdk/.vscode/mcp.stdio.container.json index 82d4e56c..e1671c40 100644 --- a/openapi-to-sdk/.vscode/mcp.stdio.container.json +++ b/openapi-to-sdk/.vscode/mcp.stdio.container.json @@ -2,8 +2,8 @@ "inputs": [ { "type": "promptString", - "id": "repository-root", - "description": "Enter the absolute path to the repository root directory (where 'openapi-to-sdk' folder is located)." + "id": "consoleapp-project-path", + "description": "The absolute path to the console app project Directory" } ], "servers": { @@ -15,10 +15,11 @@ "-i", "--rm", "-v", - "${input:repository-root}/workspace:/app/workspace", + "${input:consoleapp-project-path}/../../workspace:/app/workspace", "-e", - "HOST_ROOT_PATH=${input:repository-root}", - "openapi-to-sdk:latest" + "HOST_ROOT_PATH=${input:consoleapp-project-path}/../../", + "openapi-to-sdk:latest", + "-c" ] } } diff --git a/openapi-to-sdk/infra/resources.bicep b/openapi-to-sdk/infra/resources.bicep index 9c484b65..3c4dc837 100644 --- a/openapi-to-sdk/infra/resources.bicep +++ b/openapi-to-sdk/infra/resources.bicep @@ -169,7 +169,8 @@ module mcpOpenApiToSdk 'br/public:avm/res/app/container-app:0.8.0' = { } ] args: [ - '--http' + '--http', + '--azure' ] // Mount the volume to a path inside the container diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Configurations/OpenApiToSdkAppSettings.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Configurations/OpenApiToSdkAppSettings.cs index c63b64e9..f86d6a34 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Configurations/OpenApiToSdkAppSettings.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Configurations/OpenApiToSdkAppSettings.cs @@ -4,8 +4,7 @@ namespace McpSamples.OpenApiToSdk.HybridApp.Configurations; /// -/// Represents the application settings for the OpenApiToSdk app. -/// Inherits from Shared AppSettings to maintain consistency. +/// This represents the application settings for the OpenApiToSdk app. /// public class OpenApiToSdkAppSettings : AppSettings { @@ -17,9 +16,10 @@ public class OpenApiToSdkAppSettings : AppSettings Description = "An MCP server that generates client SDKs from OpenAPI specifications using Kiota." }; - // -------------------------------------------------------- - // Runtime Configurations (Values assigned after calculation in Program.cs) - // -------------------------------------------------------- + /// + /// Gets or sets the instance. + /// + public RuntimeSettings Runtime { get; set; } = new RuntimeSettings(); /// /// The root path for the workspace (shared volume or local folder). @@ -41,13 +41,42 @@ public class OpenApiToSdkAppSettings : AppSettings /// public bool IsHttpMode { get; set; } - /// - /// Indicates if the app is running inside a Docker container. - /// - public bool IsContainer { 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]; + switch (arg) + { + case "--azure": + case "-a": + (settings as OpenApiToSdkAppSettings)!.Runtime.Mode = "Azure"; + break; + case "--container": + case "-c": + (settings as OpenApiToSdkAppSettings)!.Runtime.Mode = "Container"; + break; + + default: + break; + } + } + + return settings; + } +} + +/// +/// This represents the runtime settings for the OpenApiToSdk app. +/// +public class RuntimeSettings +{ /// - /// Indicates if the app is running in Azure Container Apps. + /// Gets or sets the runtime mode (Local, Container, Azure). /// - public bool IsAzure { get; set; } + public string Mode { get; set; } = "Local"; } \ No newline at end of file diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs index 16e5742f..884b02de 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Program.cs @@ -6,10 +6,6 @@ using McpSamples.Shared.Configurations; using McpSamples.Shared.Extensions; -/// -/// This is the entry point for the OpenAPI to SDK Hybrid App. -/// It configures the application host, registers services, and sets up runtime settings. -/// var useStreamableHttp = AppSettings.UseStreamableHttp(Environment.GetEnvironmentVariables(), args); IHostApplicationBuilder builder = useStreamableHttp @@ -17,7 +13,6 @@ : Host.CreateApplicationBuilder(args); builder.Services.AddAppSettings(builder.Configuration, args); - builder.Services.AddHttpContextAccessor(); var options = new JsonSerializerOptions @@ -72,35 +67,24 @@ return Results.Ok(new { Message = "File uploaded successfully.", SavedPath = filePath }); }) .DisableAntiforgery(); - } await app.RunAsync(); -/// -/// Initializes runtime settings based on the execution environment (Local/Docker/Azure). -/// -/// The instance. -/// A boolean indicating if the application is running in HTTP mode. void InitializeRuntimeSettings(OpenApiToSdkAppSettings settings, bool isHttp) { - bool isContainer = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true"; - string? azureAppName = Environment.GetEnvironmentVariable("CONTAINER_APP_NAME"); - bool isAzure = !string.IsNullOrEmpty(azureAppName); - string baseDirectory; - if (isContainer) - { - baseDirectory = "/app"; - } - else + if (settings.Runtime.Mode.Equals("Local", StringComparison.OrdinalIgnoreCase)) { baseDirectory = TryFindProjectRoot(Directory.GetCurrentDirectory()) ?? Directory.GetCurrentDirectory(); - baseDirectory = Path.Combine(baseDirectory, "openapi-to-sdk"); Console.WriteLine($"[Init] Local Base Directory resolved to: {baseDirectory}"); } + else + { + baseDirectory = Directory.GetCurrentDirectory(); + } string workspacePath = Path.Combine(baseDirectory, "workspace"); @@ -108,25 +92,18 @@ void InitializeRuntimeSettings(OpenApiToSdkAppSettings settings, bool isHttp) settings.GeneratedPath = Path.Combine(workspacePath, "generated"); settings.SpecsPath = Path.Combine(workspacePath, "specs"); settings.IsHttpMode = isHttp; - settings.IsContainer = isContainer; - settings.IsAzure = isAzure; if (!Directory.Exists(settings.WorkspacePath)) Directory.CreateDirectory(settings.WorkspacePath); if (!Directory.Exists(settings.SpecsPath)) Directory.CreateDirectory(settings.SpecsPath); if (!Directory.Exists(settings.GeneratedPath)) Directory.CreateDirectory(settings.GeneratedPath); } -/// -/// Helper method to find the project root directory by searching for 'Dockerfile.openapi-to-sdk'. -/// -/// The path to start searching from. -/// The full path of the project root if found, otherwise null. string? TryFindProjectRoot(string startPath) { var dir = new DirectoryInfo(startPath); while (dir != null) { - if (dir.GetFiles("Dockerfile.openapi-to-sdk").Length > 0) + if (dir.Name.Equals("openapi-to-sdk", StringComparison.OrdinalIgnoreCase)) { return dir.FullName; } diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs index e598905c..ea2f20ad 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/Services/OpenApiService.cs @@ -19,6 +19,7 @@ public async Task GenerateSdkAsync(string specSource, string language, s { if (string.IsNullOrWhiteSpace(specSource)) throw new ArgumentException("Spec source cannot be empty.", nameof(specSource)); + var finalClassName = string.IsNullOrWhiteSpace(clientClassName) ? "ApiClient" : clientClassName; var finalNamespace = string.IsNullOrWhiteSpace(namespaceName) ? "ApiSdk" : namespaceName; var finalOptions = additionalOptions ?? string.Empty; @@ -46,7 +47,16 @@ The output path is managed automatically by the server. } else { - if (settings.IsContainer || settings.IsAzure) + if (settings.Runtime.Mode == "Local") + { + inputPath = specSource; + + if (!File.Exists(inputPath)) + { + throw new FileNotFoundException($"Local file not found at: {inputPath}. Please check the path."); + } + } + else { string fileName = Path.GetFileName(specSource); if (fileName.Contains('\\')) @@ -58,137 +68,107 @@ The output path is managed automatically by the server. if (!File.Exists(inputPath)) { - if (settings.IsAzure) + switch (settings.Runtime.Mode) { - string uploadUrl = "/upload"; - var request = httpContextAccessor.HttpContext?.Request; - if (request != null) - { - uploadUrl = $"{request.Scheme}://{request.Host}/upload"; - } + case "Azure": + string uploadUrl = "/upload"; + var request = httpContextAccessor.HttpContext?.Request; + if (request != null) + { + uploadUrl = $"{request.Scheme}://{request.Host}/upload"; + } - return $$""" - Action Required: File Upload Needed (Azure) + return $$""" + Action Required: File Upload Needed (Azure) - I am running in an Azure environment and cannot access your local file `{{specSource}}` directly. - To proceed, you need to upload the file to the server. + I am running in an Azure environment and cannot access your local file `{{specSource}}` 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: + Please execute the appropriate command for your operating system (OS) using your terminal or shell tool: - ### macOS & Linux - curl -F "file=@{{specSource}}" {{uploadUrl}} + ### macOS & Linux + curl -F "file=@{{specSource}}" {{uploadUrl}} - ### Windows (PowerShell) - Invoke-RestMethod -Uri "{{uploadUrl}}" -Method Post -Form @{file = Get-Item -Path "{{specSource}}"} + ### Windows (PowerShell) + Invoke-RestMethod -Uri "{{uploadUrl}}" -Method Post -Form @{ file = Get-Item -Path "{{specSource}}" } - After the command completes successfully, please call this `generate_sdk` tool again. - """; - } - if (settings.IsContainer) - { - string targetHostPath = $"workspace/specs/{fileName}"; + After the command completes successfully, please call this `generate_sdk` tool again. + """; + case "Container": + string targetHostPath = $"workspace/specs/{fileName}"; + string? hostRootPath = Environment.GetEnvironmentVariable("HOST_ROOT_PATH"); - string? hostRootPath = Environment.GetEnvironmentVariable("HOST_ROOT_PATH"); + if (!string.IsNullOrEmpty(hostRootPath)) + { + string normalizedHostRoot = hostRootPath.Replace('\\', '/').TrimEnd('/'); + targetHostPath = $"{normalizedHostRoot}/workspace/specs/{fileName}"; + } - if (!string.IsNullOrEmpty(hostRootPath)) - { - string normalizedHostRoot = hostRootPath.Replace('\\', '/').TrimEnd('/'); + return $""" + Action Required: File Synchronization Needed - targetHostPath = $"{normalizedHostRoot}/workspace/specs/{fileName}"; - } + The file '{fileName}' is not currently in the shared volume visible to the container. + To proceed with SDK generation, I need access to this file. - return $""" - Action Required: File Synchronization Needed + EXECUTION PLAN (Follow Strictly): + 1. COPY the file from `{specSource}` to `{targetHostPath}`. + 2. RETRY the `generate_sdk` tool immediately with the same arguments. + """; - The file '{fileName}' is not currently in the shared volume visible to the container. - To proceed with SDK generation, I need access to this file. - - EXECUTION PLAN (Follow Strictly): - 1. COPY the file from `{specSource}` to `{targetHostPath}`. - 2. RETRY the `generate_sdk` tool immediately with the same arguments. - """; + default: + throw new FileNotFoundException($"File not found in volume: {inputPath}"); } } } - else - { - inputPath = specSource; - if (!File.Exists(inputPath)) - { - var errorMsg = $"[Error] Local file not found: {inputPath}"; - logger.LogError(errorMsg); - return errorMsg; - } - } - logger.LogInformation("Input is a File: {InputPath}", inputPath); } - string outputId = Guid.NewGuid().ToString(); - string tempOutputPath = Path.Combine(settings.GeneratedPath, outputId); - Directory.CreateDirectory(tempOutputPath); + var outputId = Guid.NewGuid().ToString(); + var tempOutputPath = Path.Combine(settings.GeneratedPath, outputId); - try + if (!Directory.Exists(tempOutputPath)) { - logger.LogInformation("Starting Kiota generation..."); - - var startInfo = new ProcessStartInfo - { - FileName = "kiota", - Arguments = $"generate -l {language} -c {finalClassName} -n {finalNamespace} -d \"{inputPath}\" -o \"{tempOutputPath}\" {finalOptions}", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using var process = new Process { StartInfo = startInfo }; - process.Start(); - await process.WaitForExitAsync(cancellationToken); - - if (process.ExitCode != 0) - { - string error = await process.StandardError.ReadToEndAsync(cancellationToken); - logger.LogError("Kiota generation failed: {Error}", error); - return $"[Error] Kiota generation failed:\n{error}"; - } + Directory.CreateDirectory(tempOutputPath); + } - string zipFileName = $"sdk-{language}-{outputId.Substring(0, 4)}.zip"; - string zipFilePath = Path.Combine(settings.GeneratedPath, zipFileName); + var startInfo = new ProcessStartInfo + { + FileName = "kiota", + Arguments = $"generate -l {language} -c {finalClassName} -n {finalNamespace} -d \"{inputPath}\" -o \"{tempOutputPath}\" {finalOptions}", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) + { + throw new InvalidOperationException("Failed to start Kiota process."); + } - ZipFile.CreateFromDirectory(tempOutputPath, zipFilePath); - logger.LogInformation("SDK generated and zipped at: {ZipFilePath}", zipFilePath); + var stdout = await process.StandardOutput.ReadToEndAsync(cancellationToken); + var stderr = await process.StandardError.ReadToEndAsync(cancellationToken); + await process.WaitForExitAsync(cancellationToken); - return CreateResultMessage(zipFileName, zipFilePath); + if (process.ExitCode != 0) + { + logger.LogError("Kiota failed: {StdErr}", stderr); + return $"[Error] Kiota generation failed:\n{stderr}\n{stdout}"; } - catch (Exception ex) + + string zipFileName = $"sdk-{language}-{outputId.Substring(0, 8)}.zip"; + string localZipPath = Path.Combine(settings.GeneratedPath, zipFileName); + + ZipFile.CreateFromDirectory(tempOutputPath, localZipPath); + + try { - logger.LogError(ex, "An unexpected error occurred during SDK generation."); - return $"[Error] An unexpected error occurred: {ex.Message}"; + Directory.Delete(tempOutputPath, true); } - finally + catch { - if (Directory.Exists(tempOutputPath)) - { - try - { - Directory.Delete(tempOutputPath, true); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Failed to clean up temp directory: {TempPath}", tempOutputPath); - } - } } - } - /// - /// Creates a result message for the SDK generation, including download links for HTTP mode. - /// - /// The name of the generated ZIP file. - /// The local path to the generated ZIP file. - /// A formatted string message with download information. - private string CreateResultMessage(string zipFileName, string localZipPath) - { if (settings.IsHttpMode) { string relativePath = $"/download/{zipFileName}"; @@ -213,14 +193,13 @@ private string CreateResultMessage(string zipFileName, string localZipPath) { string finalPath = localZipPath; - if (settings.IsContainer) + if (settings.Runtime.Mode == "Container") { string? hostRootPath = Environment.GetEnvironmentVariable("HOST_ROOT_PATH"); if (!string.IsNullOrEmpty(hostRootPath)) { string relativePathFromApp = finalPath.Substring("/app".Length).TrimStart('/'); - string hostPathNormalized = hostRootPath.TrimEnd('/', '\\'); finalPath = $"{hostPathNormalized}/{relativePathFromApp}"; diff --git a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/appsettings.json b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/appsettings.json index 65ff64ad..62963f5c 100644 --- a/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/appsettings.json +++ b/openapi-to-sdk/src/McpSamples.OpenApiToSdk.HybridApp/appsettings.json @@ -6,5 +6,8 @@ } }, "AllowedHosts": "*", - "UseHttp": false + "UseHttp": false, + "Runtime": { + "Mode": "Local" + } } \ No newline at end of file From 44bb271f2558c0636dbb6bffbb7feda897f735bd Mon Sep 17 00:00:00 2001 From: x-or-b Date: Sat, 13 Dec 2025 02:51:05 +0900 Subject: [PATCH 61/61] Add README --- openapi-to-sdk/README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openapi-to-sdk/README.md b/openapi-to-sdk/README.md index 0754316c..bd76a06e 100644 --- a/openapi-to-sdk/README.md +++ b/openapi-to-sdk/README.md @@ -102,12 +102,12 @@ OpenAPI to SDK MCP server includes: ```bash # use local container image - docker run -i --rm -p 8080:8080 -v "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace" -e HOST_ROOT_PATH="$REPOSITORY_ROOT/openapi-to-sdk" openapi-to-sdk:latest --http + docker run -i --rm -p 8080:8080 -v "$REPOSITORY_ROOT/openapi-to-sdk/workspace:/app/workspace" -e HOST_ROOT_PATH="$REPOSITORY_ROOT/openapi-to-sdk" openapi-to-sdk:latest --http -c ``` #### On Azure @@ -220,8 +220,7 @@ OpenAPI to SDK MCP server includes: 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 `openapi-to-sdk` then click `Start Server`. 1. When prompted, enter one of the following values: - - The absolute directory path of the `McpSamples.OpenApiToSdk.HybridApp` project (On a local machine) - - The absolute directory path of the `openapi-to-sdk` project (In a container) + - The absolute directory path of the `McpSamples.OpenApiToSdk.HybridApp` project - The FQDN of Azure Container Apps. 1. Use a prompt by typing `/mcp.openapi-to-sdk.generate_sdk` and enter keywords to search. You'll get a prompt like: