Skip to content

Commit 3cf09f9

Browse files
Implementation of Custom Tool in MCP (#3048)
## Why make this change? - Closes on - #2877 ## What is this change? This pull request introduces a new system for dynamically generating and registering custom MCP tools based on stored procedure entity configurations in the runtime configuration. The main changes are the implementation of the `DynamicCustomTool` class, a factory to create these tools from configuration, and the necessary service registration logic to ensure these custom tools are available at runtime. **Dynamic custom MCP tool support:** * Added the `DynamicCustomTool` class, which implements `IMcpTool` and provides logic for generating tool metadata, validating configuration, handling authorization, executing the underlying stored procedure, and formatting the response. This enables each stored procedure entity with `custom-tool` enabled to be exposed as a dedicated MCP tool. * Introduced the `CustomMcpToolFactory` class, which scans the runtime configuration for stored procedure entities marked with `custom-tool` enabled and creates corresponding `DynamicCustomTool` instances. **Dependency injection and service registration:** * Updated the MCP server startup (`AddDabMcpServer`) to register custom tools generated from configuration by calling a new `RegisterCustomTools` method after auto-discovering static tools. * Modified the `RegisterAllMcpTools` method to exclude `DynamicCustomTool` from auto-discovery (since these are created dynamically per configuration) and added the `RegisterCustomTools` method to register each generated custom tool as a singleton service. ## How was this tested? - [x] Unit Tests - [x] Manual Tests using Insomnia and VS code GHCP chat ## Sample Request(s) 1. List All Tools (also includes custom tool) ``` { "jsonrpc": "2.0", "method": "tools/list", "params": {}, "id": 1 } ``` 2. Get Books (no parameters) ``` { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "get_books" }, "id": 2 } ``` 3. Get Book by ID ``` { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "get_book", "arguments": { "id": 1 } }, "id": 3 } ``` 4. Insert Book ``` { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "insert_book", "arguments": { "title": "Test Book from MCP", "publisher_id": "1234" } }, "id": 4 } ``` 5. Count Books (no parameters) ``` { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "count_books" }, "id": 5 } ``` Error Scenarios 6. Missing Required Parameter ``` { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "get_book" }, "id": 6 } ``` 7. Non-Existent Tool ``` { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "non_existent_tool" }, "id": 7 } ``` 8. Invalid Foreign Key ``` { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "insert_book", "arguments": { "title": "Test Book", "publisher_id": "999999" } }, "id": 8 } ``` Edge Cases 9. SQL Injection Attempt ``` { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "get_book", "arguments": { "id": "1; DROP TABLE books; --" } }, "id": 9 } ``` 11. Special Characters ``` { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "insert_book", "arguments": { "title": "Test Book with 'quotes' and \"double quotes\" and <tags>", "publisher_id": "1234" } }, "id": 10 } ``` 12. Empty String ``` { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "insert_book", "arguments": { "title": "", "publisher_id": "1234" } }, "id": 11 } ``` 13. Invalid Type (string for int) ``` { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "get_book", "arguments": { "id": "not_a_number" } }, "id": 12 } ``` 14. Negative ID ``` { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "get_book", "arguments": { "id": -1 } }, "id": 13 } ``` 15. Maximum Integer ``` { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "get_book", "arguments": { "id": 2147483647 } }, "id": 14 } ``` 16. Case Sensitivity (should fail) ``` { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "GET_BOOKS" }, "id": 15 } ``` --------- Co-authored-by: Aniruddh Munde <[email protected]>
1 parent a62cb9e commit 3cf09f9

5 files changed

Lines changed: 868 additions & 1 deletion

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Azure.DataApiBuilder.Config.ObjectModel;
5+
using Azure.DataApiBuilder.Mcp.Model;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace Azure.DataApiBuilder.Mcp.Core
9+
{
10+
/// <summary>
11+
/// Factory for creating custom MCP tools from stored procedure entity configurations.
12+
/// Scans runtime configuration and generates dynamic tools for entities marked with custom-tool enabled.
13+
/// </summary>
14+
public class CustomMcpToolFactory
15+
{
16+
/// <summary>
17+
/// Creates custom MCP tools from entities configured with "mcp": { "custom-tool": true }.
18+
/// </summary>
19+
/// <param name="config">The runtime configuration containing entity definitions.</param>
20+
/// <param name="logger">Optional logger for diagnostic information.</param>
21+
/// <returns>Enumerable of custom tools generated from configuration.</returns>
22+
public static IEnumerable<IMcpTool> CreateCustomTools(RuntimeConfig config, ILogger? logger = null)
23+
{
24+
if (config.Entities == null)
25+
{
26+
logger?.LogWarning("No entities found in runtime configuration for custom tool generation.");
27+
return Enumerable.Empty<IMcpTool>();
28+
}
29+
30+
List<IMcpTool> customTools = new();
31+
32+
foreach ((string entityName, Entity entity) in config.Entities)
33+
{
34+
// Filter: Only stored procedures with custom-tool enabled
35+
if (entity.Source.Type == EntitySourceType.StoredProcedure &&
36+
entity.Mcp?.CustomToolEnabled == true)
37+
{
38+
try
39+
{
40+
DynamicCustomTool tool = new(entityName, entity);
41+
42+
logger?.LogInformation(
43+
"Created custom MCP tool '{ToolName}' for stored procedure entity '{EntityName}'",
44+
tool.GetToolMetadata().Name,
45+
entityName);
46+
47+
customTools.Add(tool);
48+
}
49+
catch (Exception ex)
50+
{
51+
logger?.LogError(
52+
ex,
53+
"Failed to create custom tool for entity '{EntityName}'. Skipping.",
54+
entityName);
55+
}
56+
}
57+
}
58+
59+
logger?.LogInformation("Custom MCP tool generation complete. Created {Count} custom tools.", customTools.Count);
60+
return customTools;
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)