Skip to content

Commit a2f779c

Browse files
souvikghosh04Jerry NixonranishanRubenCerna2079Copilot
authored
Adding MCP capability in DAB (#2868)
## Why make this change? - The linked issue proposes integrating an MCP (Model Context Protocol) server so AI/agent tooling (e.g., VS Code / Copilot–style agents) can introspect the configured data model and perform safe, structured operations against DAB-managed data sources. - This enables richer developer tooling, lowers friction for exploratory data access, and creates a foundation for future AI-assisted authoring and governance scenarios. ## What is this change? ### Introduces an MCP service layer that exposes: Schema / entity metadata derived from the existing DAB configuration (tables, stored procedures, relationships, GraphQL entity projections). Operation capabilities (read / create / update / delete) aligned with DAB authorization rules. A capability negotiation / handshake endpoint so MCP clients can discover features. MCP endpoint can be accessed with `/mcp` Sample request to discover tools- ``` POST: http://localhost:5000/mcp { "jsonrpc": "2.0", "id": "1", "method": "tools/list" } ``` ## How was this tested? The working of the MCP endpoint and the describe-entities tool is tested manually in the local environment. - Enable MCP in dab-config.json `"mcp": { "enabled": true, "path": "/mcp", "dml-tools": { "describe-entities": true } }` - The server was started locally and confirmed to be listening on `http://localhost:5000`. - Send a POST request to the MCP endpoint, `http://localhost:5000/mcp` - Use the Sample Requests shared for the body of the request. - The tools/list request successfully returned all registered tools, confirming that the MCP server and tool registry were initialized correctly. - The tools/call request for describe-entities returned the expected entity metadata. ## Sample Request(s) 1. Listing all tools available. `{ "jsonrpc": "2.0", "id": "1", "method": "tools/list", "params": {} }` 2. Use the describe-entities tool `{ "jsonrpc": "2.0", "id": "1", "method": "tools/call", "params": { "name": "describe-entities" } }` --------- Co-authored-by: Jerry Nixon <[email protected]> Co-authored-by: Rahul Nishant <[email protected]> Co-authored-by: RubenCerna2079 <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: Aniruddh Munde <[email protected]> Co-authored-by: Ruben Cerna <[email protected]> Co-authored-by: Anusha Kolan <[email protected]>
1 parent 2834f70 commit a2f779c

106 files changed

Lines changed: 1970 additions & 127 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

schemas/dab.draft.schema.json

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,13 @@
158158
"type": "object",
159159
"properties": {
160160
"max-page-size": {
161-
"type": ["integer", "null"],
161+
"type": [ "integer", "null" ],
162162
"description": "Defines the maximum number of records that can be returned in a single page of results. If set to null, the default value is 100,000.",
163163
"default": 100000,
164164
"minimum": 1
165165
},
166166
"default-page-size": {
167-
"type": ["integer", "null"],
167+
"type": [ "integer", "null" ],
168168
"description": "Sets the default number of records returned in a single response. When this limit is reached, a continuation token is provided to retrieve the next page. If set to null, the default value is 100.",
169169
"default": 100,
170170
"minimum": 1
@@ -214,7 +214,7 @@
214214
"description": "Allow enabling/disabling GraphQL requests for all entities."
215215
},
216216
"depth-limit": {
217-
"type": ["integer", "null"],
217+
"type": [ "integer", "null" ],
218218
"description": "Maximum allowed depth of a GraphQL query.",
219219
"default": null
220220
},
@@ -239,26 +239,87 @@
239239
}
240240
}
241241
},
242+
"mcp": {
243+
"type": "object",
244+
"description": "Global MCP endpoint configuration",
245+
"additionalProperties": false,
246+
"properties": {
247+
"path": {
248+
"default": "/mcp",
249+
"type": "string"
250+
},
251+
"enabled": {
252+
"type": "boolean",
253+
"description": "Allow enabling/disabling MCP requests for all entities.",
254+
"default": true
255+
},
256+
"dml-tools": {
257+
"oneOf": [
258+
{
259+
"type": "boolean",
260+
"description": "Enable/disable all DML tools with default settings."
261+
},
262+
{
263+
"type": "object",
264+
"description": "Individual DML tools configuration",
265+
"additionalProperties": false,
266+
"properties": {
267+
"describe-entities": {
268+
"type": "boolean",
269+
"description": "Enable/disable the describe-entities tool.",
270+
"default": false
271+
},
272+
"create-record": {
273+
"type": "boolean",
274+
"description": "Enable/disable the create-record tool.",
275+
"default": false
276+
},
277+
"read-records": {
278+
"type": "boolean",
279+
"description": "Enable/disable the read-records tool.",
280+
"default": false
281+
},
282+
"update-record": {
283+
"type": "boolean",
284+
"description": "Enable/disable the update-record tool.",
285+
"default": false
286+
},
287+
"delete-record": {
288+
"type": "boolean",
289+
"description": "Enable/disable the delete-record tool.",
290+
"default": false
291+
},
292+
"execute-entity": {
293+
"type": "boolean",
294+
"description": "Enable/disable the execute-entity tool.",
295+
"default": false
296+
}
297+
}
298+
}
299+
]
300+
}
301+
}
302+
},
242303
"host": {
243304
"type": "object",
244305
"description": "Global hosting configuration",
245306
"additionalProperties": false,
246307
"properties": {
247308
"max-response-size-mb": {
248-
"type": ["integer", "null"],
309+
"type": [ "integer", "null" ],
249310
"description": "Specifies the maximum size, in megabytes, of the database response allowed in a single result. If set to null, the default value is 158 MB.",
250311
"default": 158,
251312
"minimum": 1,
252313
"maximum": 158
253314
},
254315
"mode": {
255316
"description": "Set if running in Development or Production mode",
256-
"type": ["string", "null"],
317+
"type": [ "string", "null" ],
257318
"default": "production",
258-
"enum": ["production", "development"]
319+
"enum": [ "production", "development" ]
259320
},
260321
"cors": {
261-
"type": ["object", "null"],
322+
"type": [ "object", "null" ],
262323
"description": "Configure CORS",
263324
"additionalProperties": false,
264325
"properties": {
@@ -278,7 +339,7 @@
278339
}
279340
},
280341
"authentication": {
281-
"type": ["object", "null"],
342+
"type": [ "object", "null" ],
282343
"additionalProperties": false,
283344
"properties": {
284345
"provider": {
@@ -322,7 +383,7 @@
322383
"type": "string"
323384
}
324385
},
325-
"required": ["audience", "issuer"]
386+
"required": [ "audience", "issuer" ]
326387
}
327388
},
328389
"allOf": [
@@ -338,9 +399,9 @@
338399
]
339400
}
340401
},
341-
"required": ["provider"]
402+
"required": [ "provider" ]
342403
},
343-
"then": { "required": ["jwt"] },
404+
"then": { "required": [ "jwt" ] },
344405
"else": { "properties": { "jwt": false } }
345406
}
346407
]
@@ -382,7 +443,7 @@
382443
"default": true
383444
}
384445
},
385-
"required": ["connection-string"]
446+
"required": [ "connection-string" ]
386447
},
387448
"open-telemetry": {
388449
"type": "object",
@@ -405,15 +466,15 @@
405466
"type": "string",
406467
"description": "Open Telemetry protocol",
407468
"default": "grpc",
408-
"enum": ["grpc", "httpprotobuf"]
469+
"enum": [ "grpc", "httpprotobuf" ]
409470
},
410471
"enabled": {
411472
"type": "boolean",
412473
"description": "Allow enabling/disabling Open Telemetry.",
413474
"default": true
414475
}
415476
},
416-
"required": ["endpoint"]
477+
"required": [ "endpoint" ]
417478
},
418479
"azure-log-analytics": {
419480
"type": "object",
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="ModelContextProtocol" />
11+
<PackageReference Include="ModelContextProtocol.AspNetCore" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="..\Core\Azure.DataApiBuilder.Core.csproj" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<Folder Include="CustomTools\" />
20+
</ItemGroup>
21+
22+
</Project>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json;
5+
using Azure.DataApiBuilder.Mcp.Model;
6+
using ModelContextProtocol.Protocol;
7+
using static Azure.DataApiBuilder.Mcp.Model.McpEnums;
8+
9+
namespace Azure.DataApiBuilder.Mcp.BuiltInTools
10+
{
11+
public class CreateRecordTool : IMcpTool
12+
{
13+
public ToolType ToolType { get; } = ToolType.BuiltIn;
14+
15+
public Tool GetToolMetadata()
16+
{
17+
return new Tool
18+
{
19+
Name = "create-record",
20+
Description = "Creates a new record in the specified entity.",
21+
InputSchema = JsonSerializer.Deserialize<JsonElement>(
22+
@"{
23+
""type"": ""object"",
24+
""properties"": {
25+
""entity"": {
26+
""type"": ""string"",
27+
""description"": ""The name of the entity""
28+
},
29+
""data"": {
30+
""type"": ""object"",
31+
""description"": ""The data for the new record""
32+
}
33+
},
34+
""required"": [""entity"", ""data""]
35+
}"
36+
)
37+
};
38+
}
39+
40+
public Task<CallToolResult> ExecuteAsync(
41+
JsonDocument? arguments,
42+
IServiceProvider serviceProvider,
43+
CancellationToken cancellationToken = default)
44+
{
45+
if (arguments == null)
46+
{
47+
return Task.FromResult(new CallToolResult
48+
{
49+
Content = [new TextContentBlock { Type = "text", Text = "Error: No arguments provided" }]
50+
});
51+
}
52+
53+
try
54+
{
55+
// Extract arguments
56+
JsonElement root = arguments.RootElement;
57+
58+
if (!root.TryGetProperty("entity", out JsonElement entityElement) ||
59+
!root.TryGetProperty("data", out JsonElement dataElement))
60+
{
61+
return Task.FromResult(new CallToolResult
62+
{
63+
Content = [new TextContentBlock { Type = "text", Text = "Error: Missing required arguments 'entity' or 'data'" }]
64+
});
65+
}
66+
67+
string entityName = entityElement.GetString() ?? string.Empty;
68+
69+
// TODO: Implement actual create logic using DAB's internal services
70+
// For now, return a placeholder response
71+
string result = $"Would create record in entity '{entityName}' with data: {dataElement.GetRawText()}";
72+
73+
return Task.FromResult(new CallToolResult
74+
{
75+
Content = [new TextContentBlock { Type = "text", Text = result }]
76+
});
77+
}
78+
catch (Exception ex)
79+
{
80+
return Task.FromResult(new CallToolResult
81+
{
82+
Content = [new TextContentBlock { Type = "text", Text = $"Error: {ex.Message}" }]
83+
});
84+
}
85+
}
86+
}
87+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json;
5+
using Azure.DataApiBuilder.Config.ObjectModel;
6+
using Azure.DataApiBuilder.Core.Configurations;
7+
using Azure.DataApiBuilder.Mcp.Model;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using ModelContextProtocol.Protocol;
10+
using static Azure.DataApiBuilder.Mcp.Model.McpEnums;
11+
12+
namespace Azure.DataApiBuilder.Mcp.BuiltInTools
13+
{
14+
public class DescribeEntitiesTool : IMcpTool
15+
{
16+
public ToolType ToolType { get; } = ToolType.BuiltIn;
17+
18+
public Tool GetToolMetadata()
19+
{
20+
return new Tool
21+
{
22+
Name = "describe-entities",
23+
Description = "Lists and describes all entities in the database."
24+
};
25+
}
26+
27+
public Task<CallToolResult> ExecuteAsync(
28+
JsonDocument? arguments,
29+
IServiceProvider serviceProvider,
30+
CancellationToken cancellationToken = default)
31+
{
32+
try
33+
{
34+
// Get the runtime config provider
35+
RuntimeConfigProvider? runtimeConfigProvider = serviceProvider.GetService<RuntimeConfigProvider>();
36+
if (runtimeConfigProvider == null || !runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig))
37+
{
38+
return Task.FromResult(new CallToolResult
39+
{
40+
Content = [new TextContentBlock { Type = "text", Text = "Error: Runtime configuration not available." }]
41+
});
42+
}
43+
44+
// Extract entity information from the runtime config
45+
Dictionary<string, object> entities = new();
46+
47+
if (runtimeConfig.Entities != null)
48+
{
49+
foreach (KeyValuePair<string, Entity> entity in runtimeConfig.Entities)
50+
{
51+
entities[entity.Key] = new
52+
{
53+
source = entity.Value.Source,
54+
permissions = entity.Value.Permissions?.Select(p => new
55+
{
56+
role = p.Role,
57+
actions = p.Actions
58+
})
59+
};
60+
}
61+
}
62+
63+
string entitiesJson = JsonSerializer.Serialize(entities, new JsonSerializerOptions
64+
{
65+
WriteIndented = true,
66+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
67+
});
68+
69+
return Task.FromResult(new CallToolResult
70+
{
71+
Content = [new TextContentBlock { Type = "application/json", Text = entitiesJson }]
72+
});
73+
}
74+
catch (Exception ex)
75+
{
76+
return Task.FromResult(new CallToolResult
77+
{
78+
Content = [new TextContentBlock { Type = "text", Text = $"Error: {ex.Message}" }]
79+
});
80+
}
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)