From 3e51f74cecd1931363164c525ca260d77a47a371 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 Aug 2025 06:36:03 +0000 Subject: [PATCH 1/3] Initial plan From a2980bf835d66624984fdee16c9b0782ff5c2a47 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 Aug 2025 06:47:04 +0000 Subject: [PATCH 2/3] Implement complex type support for McpToolProperty with examples Co-authored-by: paulyuk <1968137+paulyuk@users.noreply.github.com> --- README.md | 57 +++++++++++++++++++++- src/OrderTool.cs | 105 ++++++++++++++++++++++++++++++++++++++++ src/Program.cs | 10 +++- src/SnippetsTool.cs | 4 +- src/ToolsInformation.cs | 8 +++ 5 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 src/OrderTool.cs diff --git a/README.md b/README.md index 169500e..c860305 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ azd down ## Source Code -The function code for the `GetSnippet` and `SaveSnippet` endpoints are defined in [`SnippetsTool.cs`](./src/SnippetsTool.cs). The `McpToolsTrigger` attribute applied to the async `Run` method exposes the code function as an MCP Server. +The function code for the MCP tools are defined in [`SnippetsTool.cs`](./src/SnippetsTool.cs), [`HelloTool.cs`](./src/HelloTool.cs), and [`OrderTool.cs`](./src/OrderTool.cs). The `McpToolsTrigger` attribute applied to the function methods exposes the code function as an MCP Server. This shows the code for a few MCP server examples (get string, get object, save object): @@ -223,6 +223,61 @@ public string SaveSnippet( } ``` +### Complex Types Support + +The MCP Tools extension supports complex data types including arrays, objects, numbers, and booleans. Here's an example from [`OrderTool.cs`](./src/OrderTool.cs) that demonstrates using complex types with `McpToolProperty`: + +```csharp +[Function(nameof(ProcessOrder))] +public string ProcessOrder( + [McpToolTrigger("process_order", "Process an order with multiple items")] ToolInvocationContext context, + [McpToolProperty("order-items", ArrayPropertyType, "List of order items, each containing item ID, quantity, and price")] + List orderItems, + [McpToolProperty("customer-name", StringPropertyType, "Name of the customer placing the order")] + string customerName, + [McpToolProperty("is-urgent", BooleanPropertyType, "Whether this order should be processed urgently")] + bool isUrgent, + [McpToolProperty("discount-percent", NumberPropertyType, "Discount percentage to apply (0-100)")] + decimal discountPercent = 0 +) +{ + // Process the order with complex data types + var totalAmount = orderItems.Sum(item => item.Price * item.Quantity); + var discountAmount = totalAmount * (discountPercent / 100); + var finalAmount = totalAmount - discountAmount; + + // Return JSON result + return JsonSerializer.Serialize(new { + OrderId = Guid.NewGuid().ToString(), + TotalAmount = finalAmount, + Items = orderItems + }); +} + +[Function(nameof(ValidateOrderData))] +public string ValidateOrderData( + [McpToolTrigger("validate_order", "Validate order data structure")] ToolInvocationContext context, + [McpToolProperty("order-data", ObjectPropertyType, "Complete order data object containing all order information")] + OrderSummary orderData +) +{ + // Validate complex object data + // ... validation logic +} +``` + +### Supported Property Types + +The following property types are supported in `McpToolProperty`: + +- `StringPropertyType` - `"string"` for text data +- `ArrayPropertyType` - `"array"` for arrays and lists +- `ObjectPropertyType` - `"object"` for complex objects +- `NumberPropertyType` - `"number"` for numeric data +- `BooleanPropertyType` - `"boolean"` for true/false values + +These constants are defined in [`ToolsInformation.cs`](./src/ToolsInformation.cs). + ## Next Steps - Add [API Management](https://github.com/Azure-Samples/remote-mcp-apim-functions-python) to your MCP server diff --git a/src/OrderTool.cs b/src/OrderTool.cs new file mode 100644 index 0000000..86473dd --- /dev/null +++ b/src/OrderTool.cs @@ -0,0 +1,105 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Extensions.Mcp; +using Microsoft.Extensions.Logging; +using System.Text.Json; +using static FunctionsSnippetTool.ToolsInformation; + +namespace FunctionsSnippetTool; + +public class OrderTool(ILogger logger) +{ + public class OrderItem + { + public string ItemId { get; set; } = string.Empty; + public int Quantity { get; set; } + public decimal Price { get; set; } + } + + public class OrderSummary + { + public string OrderId { get; set; } = string.Empty; + public decimal TotalAmount { get; set; } + public List Items { get; set; } = new(); + public bool IsUrgent { get; set; } + } + + [Function(nameof(ProcessOrder))] + public string ProcessOrder( + [McpToolTrigger("process_order", "Process an order with multiple items")] + ToolInvocationContext context, + [McpToolProperty("order-items", ArrayPropertyType, "List of order items, each containing item ID, quantity, and price")] + List orderItems, + [McpToolProperty("customer-name", StringPropertyType, "Name of the customer placing the order")] + string customerName, + [McpToolProperty("is-urgent", BooleanPropertyType, "Whether this order should be processed urgently")] + bool isUrgent, + [McpToolProperty("discount-percent", NumberPropertyType, "Discount percentage to apply (0-100)")] + decimal discountPercent = 0 + ) + { + logger.LogInformation("Processing order for customer: {CustomerName}", customerName); + + var totalAmount = orderItems.Sum(item => item.Price * item.Quantity); + var discountAmount = totalAmount * (discountPercent / 100); + var finalAmount = totalAmount - discountAmount; + + var orderSummary = new OrderSummary + { + OrderId = Guid.NewGuid().ToString("N")[..8], + TotalAmount = finalAmount, + Items = orderItems, + IsUrgent = isUrgent + }; + + var result = JsonSerializer.Serialize(orderSummary, new JsonSerializerOptions { WriteIndented = true }); + + logger.LogInformation("Order processed successfully. Order ID: {OrderId}", orderSummary.OrderId); + return result; + } + + [Function(nameof(ValidateOrderData))] + public string ValidateOrderData( + [McpToolTrigger("validate_order", "Validate order data structure")] + ToolInvocationContext context, + [McpToolProperty("order-data", ObjectPropertyType, "Complete order data object containing all order information")] + OrderSummary orderData + ) + { + logger.LogInformation("Validating order data for Order ID: {OrderId}", orderData.OrderId); + + var validationResults = new List(); + + if (string.IsNullOrEmpty(orderData.OrderId)) + validationResults.Add("Order ID is required"); + + if (orderData.TotalAmount <= 0) + validationResults.Add("Total amount must be greater than zero"); + + if (orderData.Items == null || orderData.Items.Count == 0) + validationResults.Add("At least one order item is required"); + else + { + for (int i = 0; i < orderData.Items.Count; i++) + { + var item = orderData.Items[i]; + if (string.IsNullOrEmpty(item.ItemId)) + validationResults.Add($"Item {i + 1}: Item ID is required"); + if (item.Quantity <= 0) + validationResults.Add($"Item {i + 1}: Quantity must be greater than zero"); + if (item.Price < 0) + validationResults.Add($"Item {i + 1}: Price cannot be negative"); + } + } + + var isValid = validationResults.Count == 0; + var result = new + { + IsValid = isValid, + ValidationErrors = validationResults, + OrderId = orderData.OrderId + }; + + logger.LogInformation("Order validation completed. Valid: {IsValid}", isValid); + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} \ No newline at end of file diff --git a/src/Program.cs b/src/Program.cs index 1e40a08..8ca095f 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -12,6 +12,14 @@ // input bindings: builder .ConfigureMcpTool(GetSnippetToolName) - .WithProperty(SnippetNamePropertyName, PropertyType, SnippetNamePropertyDescription); + .WithProperty(SnippetNamePropertyName, StringPropertyType, SnippetNamePropertyDescription); + +// Example of configuring complex types for the order processing tool +builder + .ConfigureMcpTool("process_order") + .WithProperty("order-items", ArrayPropertyType, "List of order items, each containing item ID, quantity, and price") + .WithProperty("customer-name", StringPropertyType, "Name of the customer placing the order") + .WithProperty("is-urgent", BooleanPropertyType, "Whether this order should be processed urgently") + .WithProperty("discount-percent", NumberPropertyType, "Discount percentage to apply (0-100)"); builder.Build().Run(); diff --git a/src/SnippetsTool.cs b/src/SnippetsTool.cs index e27ceab..75fb5ef 100644 --- a/src/SnippetsTool.cs +++ b/src/SnippetsTool.cs @@ -24,9 +24,9 @@ public object GetSnippet( public string SaveSnippet( [McpToolTrigger(SaveSnippetToolName, SaveSnippetToolDescription)] ToolInvocationContext context, - [McpToolProperty(SnippetNamePropertyName, PropertyType, SnippetNamePropertyDescription)] + [McpToolProperty(SnippetNamePropertyName, StringPropertyType, SnippetNamePropertyDescription)] string name, - [McpToolProperty(SnippetPropertyName, PropertyType, SnippetPropertyDescription)] + [McpToolProperty(SnippetPropertyName, StringPropertyType, SnippetPropertyDescription)] string snippet ) { diff --git a/src/ToolsInformation.cs b/src/ToolsInformation.cs index 649243c..83fabb3 100644 --- a/src/ToolsInformation.cs +++ b/src/ToolsInformation.cs @@ -13,6 +13,14 @@ internal sealed class ToolsInformation public const string SnippetNamePropertyDescription = "The name of the snippet."; public const string SnippetPropertyDescription = "The code snippet."; public const string PropertyType = "string"; + + // Property types for MCP tools + public const string StringPropertyType = "string"; + public const string ArrayPropertyType = "array"; + public const string ObjectPropertyType = "object"; + public const string NumberPropertyType = "number"; + public const string BooleanPropertyType = "boolean"; + public const string HelloToolName = "hello"; public const string HelloToolDescription = "Simple hello world MCP Tool that responses with a hello message."; From 6ae037ddb9cef33409fc56826b40eef7b88a078f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 Aug 2025 06:57:52 +0000 Subject: [PATCH 3/3] Replace OrderTool with snippets-focused complex type examples Co-authored-by: paulyuk <1968137+paulyuk@users.noreply.github.com> --- README.md | 69 ++++++++++++++----------- src/OrderTool.cs | 105 -------------------------------------- src/SnippetsTool.cs | 120 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 135 deletions(-) delete mode 100644 src/OrderTool.cs diff --git a/README.md b/README.md index c860305..e3e99f7 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ azd down ## Source Code -The function code for the MCP tools are defined in [`SnippetsTool.cs`](./src/SnippetsTool.cs), [`HelloTool.cs`](./src/HelloTool.cs), and [`OrderTool.cs`](./src/OrderTool.cs). The `McpToolsTrigger` attribute applied to the function methods exposes the code function as an MCP Server. +The function code for the MCP tools are defined in [`SnippetsTool.cs`](./src/SnippetsTool.cs) and [`HelloTool.cs`](./src/HelloTool.cs). The `McpToolsTrigger` attribute applied to the function methods exposes the code function as an MCP Server. This shows the code for a few MCP server examples (get string, get object, save object): @@ -225,44 +225,53 @@ public string SaveSnippet( ### Complex Types Support -The MCP Tools extension supports complex data types including arrays, objects, numbers, and booleans. Here's an example from [`OrderTool.cs`](./src/OrderTool.cs) that demonstrates using complex types with `McpToolProperty`: +The MCP Tools extension supports complex data types including arrays, objects, numbers, and booleans. Here are examples from [`SnippetsTool.cs`](./src/SnippetsTool.cs) that demonstrate using complex types with `McpToolProperty`: ```csharp -[Function(nameof(ProcessOrder))] -public string ProcessOrder( - [McpToolTrigger("process_order", "Process an order with multiple items")] ToolInvocationContext context, - [McpToolProperty("order-items", ArrayPropertyType, "List of order items, each containing item ID, quantity, and price")] - List orderItems, - [McpToolProperty("customer-name", StringPropertyType, "Name of the customer placing the order")] - string customerName, - [McpToolProperty("is-urgent", BooleanPropertyType, "Whether this order should be processed urgently")] - bool isUrgent, - [McpToolProperty("discount-percent", NumberPropertyType, "Discount percentage to apply (0-100)")] - decimal discountPercent = 0 +[Function(nameof(BulkSaveSnippets))] +public string BulkSaveSnippets( + [McpToolTrigger("bulk_save_snippets", "Save multiple code snippets at once")] ToolInvocationContext context, + [McpToolProperty("snippets", ArrayPropertyType, "Array of snippet objects containing name, content, description, and tags")] + List snippets, + [McpToolProperty("overwrite-existing", BooleanPropertyType, "Whether to overwrite existing snippets with same names")] + bool overwriteExisting = false ) { - // Process the order with complex data types - var totalAmount = orderItems.Sum(item => item.Price * item.Quantity); - var discountAmount = totalAmount * (discountPercent / 100); - var finalAmount = totalAmount - discountAmount; + logger.LogInformation("Bulk saving {Count} snippets", snippets.Count); - // Return JSON result - return JsonSerializer.Serialize(new { - OrderId = Guid.NewGuid().ToString(), - TotalAmount = finalAmount, - Items = orderItems - }); + var results = new List(); + foreach (var snippet in snippets) + { + // Process each snippet in the array + results.Add(new + { + Name = snippet.Name, + Status = "Success", + Message = $"Snippet '{snippet.Name}' saved successfully" + }); + } + + return JsonSerializer.Serialize(results, new JsonSerializerOptions { WriteIndented = true }); } -[Function(nameof(ValidateOrderData))] -public string ValidateOrderData( - [McpToolTrigger("validate_order", "Validate order data structure")] ToolInvocationContext context, - [McpToolProperty("order-data", ObjectPropertyType, "Complete order data object containing all order information")] - OrderSummary orderData +[Function(nameof(SearchSnippets))] +public string SearchSnippets( + [McpToolTrigger("search_snippets", "Search for snippets using various criteria")] ToolInvocationContext context, + [McpToolProperty("search-criteria", ObjectPropertyType, "Search criteria object with tags, name pattern, and content inclusion options")] + SnippetSearchCriteria searchCriteria ) { - // Validate complex object data - // ... validation logic + // Process complex object with multiple properties + var filteredResults = mockResults.AsEnumerable(); + + if (searchCriteria.Tags.Any()) + { + filteredResults = filteredResults.Where(s => + searchCriteria.Tags.Any(tag => s.Tags.Contains(tag, StringComparer.OrdinalIgnoreCase))); + } + + // Return filtered snippet results + return JsonSerializer.Serialize(filteredResults.ToList()); } ``` diff --git a/src/OrderTool.cs b/src/OrderTool.cs deleted file mode 100644 index 86473dd..0000000 --- a/src/OrderTool.cs +++ /dev/null @@ -1,105 +0,0 @@ -using Microsoft.Azure.Functions.Worker; -using Microsoft.Azure.Functions.Worker.Extensions.Mcp; -using Microsoft.Extensions.Logging; -using System.Text.Json; -using static FunctionsSnippetTool.ToolsInformation; - -namespace FunctionsSnippetTool; - -public class OrderTool(ILogger logger) -{ - public class OrderItem - { - public string ItemId { get; set; } = string.Empty; - public int Quantity { get; set; } - public decimal Price { get; set; } - } - - public class OrderSummary - { - public string OrderId { get; set; } = string.Empty; - public decimal TotalAmount { get; set; } - public List Items { get; set; } = new(); - public bool IsUrgent { get; set; } - } - - [Function(nameof(ProcessOrder))] - public string ProcessOrder( - [McpToolTrigger("process_order", "Process an order with multiple items")] - ToolInvocationContext context, - [McpToolProperty("order-items", ArrayPropertyType, "List of order items, each containing item ID, quantity, and price")] - List orderItems, - [McpToolProperty("customer-name", StringPropertyType, "Name of the customer placing the order")] - string customerName, - [McpToolProperty("is-urgent", BooleanPropertyType, "Whether this order should be processed urgently")] - bool isUrgent, - [McpToolProperty("discount-percent", NumberPropertyType, "Discount percentage to apply (0-100)")] - decimal discountPercent = 0 - ) - { - logger.LogInformation("Processing order for customer: {CustomerName}", customerName); - - var totalAmount = orderItems.Sum(item => item.Price * item.Quantity); - var discountAmount = totalAmount * (discountPercent / 100); - var finalAmount = totalAmount - discountAmount; - - var orderSummary = new OrderSummary - { - OrderId = Guid.NewGuid().ToString("N")[..8], - TotalAmount = finalAmount, - Items = orderItems, - IsUrgent = isUrgent - }; - - var result = JsonSerializer.Serialize(orderSummary, new JsonSerializerOptions { WriteIndented = true }); - - logger.LogInformation("Order processed successfully. Order ID: {OrderId}", orderSummary.OrderId); - return result; - } - - [Function(nameof(ValidateOrderData))] - public string ValidateOrderData( - [McpToolTrigger("validate_order", "Validate order data structure")] - ToolInvocationContext context, - [McpToolProperty("order-data", ObjectPropertyType, "Complete order data object containing all order information")] - OrderSummary orderData - ) - { - logger.LogInformation("Validating order data for Order ID: {OrderId}", orderData.OrderId); - - var validationResults = new List(); - - if (string.IsNullOrEmpty(orderData.OrderId)) - validationResults.Add("Order ID is required"); - - if (orderData.TotalAmount <= 0) - validationResults.Add("Total amount must be greater than zero"); - - if (orderData.Items == null || orderData.Items.Count == 0) - validationResults.Add("At least one order item is required"); - else - { - for (int i = 0; i < orderData.Items.Count; i++) - { - var item = orderData.Items[i]; - if (string.IsNullOrEmpty(item.ItemId)) - validationResults.Add($"Item {i + 1}: Item ID is required"); - if (item.Quantity <= 0) - validationResults.Add($"Item {i + 1}: Quantity must be greater than zero"); - if (item.Price < 0) - validationResults.Add($"Item {i + 1}: Price cannot be negative"); - } - } - - var isValid = validationResults.Count == 0; - var result = new - { - IsValid = isValid, - ValidationErrors = validationResults, - OrderId = orderData.OrderId - }; - - logger.LogInformation("Order validation completed. Valid: {IsValid}", isValid); - return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); - } -} \ No newline at end of file diff --git a/src/SnippetsTool.cs b/src/SnippetsTool.cs index 75fb5ef..4c15d43 100644 --- a/src/SnippetsTool.cs +++ b/src/SnippetsTool.cs @@ -1,6 +1,7 @@ using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Extensions.Mcp; using Microsoft.Extensions.Logging; +using System.Text.Json; using static FunctionsSnippetTool.ToolsInformation; namespace FunctionsSnippetTool; @@ -9,6 +10,21 @@ public class SnippetsTool(ILogger logger) { private const string BlobPath = "snippets/{mcptoolargs." + SnippetNamePropertyName + "}.json"; + public class SnippetInfo + { + public string Name { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public List Tags { get; set; } = new(); + } + + public class SnippetSearchCriteria + { + public List Tags { get; set; } = new(); + public string NamePattern { get; set; } = string.Empty; + public bool IncludeContent { get; set; } = true; + } + [Function(nameof(GetSnippet))] public object GetSnippet( [McpToolTrigger(GetSnippetToolName, GetSnippetToolDescription)] @@ -32,4 +48,108 @@ string snippet { return snippet; } + + [Function(nameof(BulkSaveSnippets))] + public string BulkSaveSnippets( + [McpToolTrigger("bulk_save_snippets", "Save multiple code snippets at once")] + ToolInvocationContext context, + [McpToolProperty("snippets", ArrayPropertyType, "Array of snippet objects containing name, content, description, and tags")] + List snippets, + [McpToolProperty("overwrite-existing", BooleanPropertyType, "Whether to overwrite existing snippets with same names")] + bool overwriteExisting = false + ) + { + logger.LogInformation("Bulk saving {Count} snippets", snippets.Count); + + var results = new List(); + foreach (var snippet in snippets) + { + try + { + // In a real implementation, you'd save to blob storage + // For demo purposes, we'll just return success status + results.Add(new + { + Name = snippet.Name, + Status = "Success", + Message = $"Snippet '{snippet.Name}' saved successfully" + }); + + logger.LogInformation("Saved snippet: {Name}", snippet.Name); + } + catch (Exception ex) + { + results.Add(new + { + Name = snippet.Name, + Status = "Error", + Message = ex.Message + }); + logger.LogError(ex, "Failed to save snippet: {Name}", snippet.Name); + } + } + + var summary = new + { + TotalProcessed = snippets.Count, + SuccessCount = results.Count(r => ((dynamic)r).Status == "Success"), + Results = results + }; + + return JsonSerializer.Serialize(summary, new JsonSerializerOptions { WriteIndented = true }); + } + + [Function(nameof(SearchSnippets))] + public string SearchSnippets( + [McpToolTrigger("search_snippets", "Search for snippets using various criteria")] + ToolInvocationContext context, + [McpToolProperty("search-criteria", ObjectPropertyType, "Search criteria object with tags, name pattern, and content inclusion options")] + SnippetSearchCriteria searchCriteria + ) + { + logger.LogInformation("Searching snippets with criteria: tags={Tags}, pattern={Pattern}", + string.Join(",", searchCriteria.Tags), searchCriteria.NamePattern); + + // In a real implementation, you'd query blob storage + // For demo purposes, return mock search results + var mockResults = new List + { + new() { Name = "hello-world", Content = "console.log('Hello World');", Description = "Basic hello world", Tags = ["javascript", "basic"] }, + new() { Name = "api-request", Content = "fetch('/api/data')", Description = "API request example", Tags = ["javascript", "api"] }, + new() { Name = "linq-query", Content = "items.Where(x => x.IsActive)", Description = "LINQ filtering", Tags = ["csharp", "linq"] } + }; + + // Apply search filters + var filteredResults = mockResults.AsEnumerable(); + + if (searchCriteria.Tags.Any()) + { + filteredResults = filteredResults.Where(s => + searchCriteria.Tags.Any(tag => s.Tags.Contains(tag, StringComparer.OrdinalIgnoreCase))); + } + + if (!string.IsNullOrEmpty(searchCriteria.NamePattern)) + { + filteredResults = filteredResults.Where(s => + s.Name.Contains(searchCriteria.NamePattern, StringComparison.OrdinalIgnoreCase)); + } + + var results = filteredResults.Select(s => new + { + s.Name, + s.Description, + s.Tags, + Content = searchCriteria.IncludeContent ? s.Content : null + }).ToList(); + + var searchResults = new + { + SearchCriteria = searchCriteria, + ResultCount = results.Count, + Results = results + }; + + logger.LogInformation("Search completed. Found {Count} matching snippets", results.Count); + return JsonSerializer.Serialize(searchResults, new JsonSerializerOptions { WriteIndented = true }); + } }