diff --git a/README.md b/README.md index 169500e..e3e99f7 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) 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): @@ -223,6 +223,70 @@ public string SaveSnippet( } ``` +### Complex Types Support + +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(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) + { + // 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(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 +) +{ + // 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()); +} +``` + +### 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/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..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)] @@ -24,12 +40,116 @@ 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 ) { 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 }); + } } 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.";