Skip to content

Conversation

anushakolan
Copy link
Contributor

@anushakolan anushakolan commented Sep 26, 2025

Why make this change?

This PR,

  • Adds the tool implementation for the update-record MCP tool in Azure.DataApiBuilder.Mcp to support updating records via MCP using keys and field payloads with full validation and authorization.
  • Adds a README to guide contributors on how to test MCP tools using the MCP Inspector.
  • Improves predicate construction logic in SqlUpdateQueryStructure.cs to support mapped column fields.

What is this change?

  • Adds docs/Testing/mcp-inspector-testing.md with step-by-step instructions for running MCP Inspector locally, bypassing TLS, and connecting to MCP endpoints.
  • Introduces UpdateRecordTool.cs under BuiltInTools, enabling record updates via MCP with full validation, authorization, and mutation engine integration.
  • Updates SqlUpdateQueryStructure.cs to construct predicates using parameterized values for safer and more flexible SQL generation.

Exceptions are thrown when:

  1. Entity is null or empty.
  2. Keys and Fields are not JSON objects.
  3. Failed to parse Keys and Fields.
  4. 0 keys provided.
  5. 0 fields provided.
  6. Key value is null or empty
  7. field value is null or empty.

Error:

  1. PermissionDenied - No permssions to execute.
  2. InvalidArguments - No arguments provided.
  3. InvalidArguments - Some missing arguments.
  4. EntityNotFound - Entity not defined in the configuration.
  5. InvalidArguments - No entity found with given key.
  6. UnexpectedError - Any other UnexpectedError.

How was this tested?

  • Manual testing via MCP Inspector
  • Integration Tests
  • Unit Tests

These scenarios were manually tested using MCP Inspector, as automated tests are not yet implemented.

Valid Cases

  1. Successful Update
  • Provided valid entity, keys, and fields.
  • Verified that values were correctly updated in the database.
  • Confirmed that the response includes all the column values similar to REST API calls.
  1. Permission Enforcement
  • Modified role permissions and verified that access control is enforced correctly.
  1. Composite Key Handling
  • Tested entities requiring multiple keys.
  • Verified correct update behavior and response formatting.
  1. Alias Mapping
  • Used mapped field name (e.g., order instead of position) and confirmed that only mapped names are accepted.

Failure Cases

  1. Empty Keys
  • Provided an empty keys object.
  • Received appropriate error indicating missing key values.
  1. Empty Fields
  • Provided an empty fields object.
  • Received error indicating that at least one field is required.
  1. Invalid Entity Name
  • Used a non-existent entity name.
  • Received EntityNotFound error.
  1. Empty Entity String
  • Provided an empty string for entity.
  • Received InvalidArguments error.
  1. Entity Not Configured
  • Used an entity not present in the configuration (e.g., Todos).
  • Received EntityNotFound error.
  1. Null Entity
  • Provided null for the entity field.
  • Received InvalidArguments error.
  1. Valid Key Format but Not in DB
  • Used correct key structure but no matching record exists.
  • Received error indicating no entity found with the given key.
  1. Invalid or Null Key Values
  • Provided null or malformed key values.
  • Received InvalidArguments error.
  1. Unauthorized Role Context
  • Removed or misconfigured role header.
  • Received PermissionDenied error.

Sample Request(s)

{ "id": "00000000-0000-0000-0000-000000000001" }
{ "title": "New Title", "order": 7 }

@anushakolan
Copy link
Contributor Author

/azp run

Copy link

Azure Pipelines successfully started running 6 pipeline(s).

@anushakolan anushakolan marked this pull request as ready for review October 1, 2025 05:22
@Copilot Copilot AI review requested due to automatic review settings October 1, 2025 05:22
@anushakolan anushakolan self-assigned this Oct 1, 2025
@anushakolan anushakolan linked an issue Oct 1, 2025 that may be closed by this pull request
6 tasks
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements the update-record MCP tool to enable updating database records via the Model Context Protocol (MCP). It includes validation, authorization, and proper error handling for various edge cases.

  • Adds a complete UpdateRecordTool implementation with comprehensive validation and error handling
  • Improves SQL predicate construction to use mapped column fields instead of exposed field names
  • Provides testing documentation for MCP Inspector usage

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
src/Azure.DataApiBuilder.Mcp/BuiltInTools/UpdateRecordTool.cs New MCP tool implementation for updating database records with full validation and authorization
src/Core/Resolvers/Sql Query Structures/SqlUpdateQueryStructure.cs Enhanced predicate construction to use backing column names for safer SQL generation
docs/Testing/mcp-inspector-testing.md Testing guide for using MCP Inspector with local development setup

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

{
if (kv.Value is null || (kv.Value is string str && string.IsNullOrWhiteSpace(str)))
{
throw new ArgumentException("Keys are required to update an entity");
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error message is not specific enough. Should indicate which key has the null/empty value, e.g., 'Key value for '{kv.Key}' cannot be null or empty.'

Suggested change
throw new ArgumentException("Keys are required to update an entity");
throw new ArgumentException($"Key value for '{kv.Key}' cannot be null or empty.");

Copilot uses AI. Check for mistakes.

Comment on lines +158 to +162
if (kvp.Value is null)
{
return BuildErrorResult("InvalidArguments", $"Primary key value for '{kvp.Key}' cannot be null.", logger);
}

Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate validation logic. The null check for key values is performed twice - once in TryParseArguments (lines 286-292) and again here. Consider consolidating this validation to avoid duplication.

Suggested change
if (kvp.Value is null)
{
return BuildErrorResult("InvalidArguments", $"Primary key value for '{kvp.Key}' cannot be null.", logger);
}

Copilot uses AI. Check for mistakes.

Copy link
Collaborator

@Aniruddh25 Aniruddh25 Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this suggestion from copilot is valid and should be addressed

@anushakolan
Copy link
Contributor Author

/azp run

Copy link

Azure Pipelines successfully started running 6 pipeline(s).


### 4. **How to use the tool**
- Set the transport type "Streamable HTTP".
- Set the URL "http://localhost:5000/mcp" and hit connect.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention as pre-requisite that dab mcp-server should be running before hitting connect here.

set NODE_TLS_REJECT_UNAUTHORIZED=0

### 3. ** Open the inspector with pre-filled token.**
http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=<token>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this token used for?

Steps to run and test MCP tools using the https://www.npmjs.com/package/@modelcontextprotocol/inspector.

### 1. **Install MCP Inspector**
npx @modelcontextprotocol/inspector
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is nodejs a pre-requisite to be installed?

{
Name = "update_record",
Description = "Updates an existing record in the specified entity. Requires 'keys' to locate the record and 'fields' to specify new values.",
InputSchema = JsonSerializer.Deserialize<JsonElement>(
Copy link
Collaborator

@Aniruddh25 Aniruddh25 Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#2829 - please update all the references of update-entity in the issue to update_record

predicate = new(
new PredicateOperand(
new Column(tableSchema: DatabaseObject.SchemaName, tableName: DatabaseObject.Name, param.Key)),
new Column(tableSchema: DatabaseObject.SchemaName, tableName: DatabaseObject.Name, backingColumn)),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems to have been a bug here. Good catch!

return BuildErrorResult(
"UnexpectedError",
ex.Message ?? "An unexpected error occurred during the update operation.",
logger: null);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why pass a null to the logger here? why not pass the logger obtained on line 85

return BuildErrorResult("PermissionDenied", "Permission denied: unable to resolve a valid role context for update operation.", logger);
}

if (!TryResolveAuthorizedRole(httpContext, authResolver, entityName, out string? effectiveRole, out string authError))
Copy link
Contributor

@aaronburtle aaronburtle Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is effectiveRole being used for here?

return BuildErrorResult("EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger);
}

// 4) Authorization after we have a known entity
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to ensure the ClientRoleHeaderAuthenticationHeaderMiddleware is invoked

public class ClientRoleHeaderAuthenticationMiddleware

before Authorization resolver kicks in here.

You should perform some testing using the simulator to check what happens for authenticated roles. anonymous role is by default.


if (string.IsNullOrWhiteSpace(roleHeader))
{
error = "Client role header is missing or empty.";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the ClientRoleHeaderAuthenticationMiddleware isn't invoked, the roleheader here will be empty. Please test that out.

return true;
}

private static bool TryResolveAuthorizedRole(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private static bool TryResolveAuthorizedRole(
private static bool TryResolveAuthorizedRoleHasPermission(

context.PrimaryKeyValuePairs[kvp.Key] = kvp.Value;
}

if (context.DatabaseObject.SourceType is EntitySourceType.Table)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this validation restricted to entity type of Table?

insertPayloadRoot: upsertPayloadRoot,
operationType: EntityActionOperation.UpdateIncremental);

context.UpdateReturnFields(keys.Keys.Concat(fields.Keys).Distinct().ToList());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we decided to return the entire record in the response to align with what we have in REST. In REST, do we only return the keys?

{
return BuildErrorResult(
"InvalidArguments",
"No entity found with the given key.",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"No entity found with the given key.",
"No record found with the given key.",

valueArray.ValueKind == JsonValueKind.Array &&
valueArray.GetArrayLength() > 0)
{
JsonElement firstItem = valueArray[0];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is assuming there will only be one element in the Value Array.

Dictionary<string, object?> normalized = new()
{
["status"] = "success",
["result"] = filteredResult // only requested values
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are no longer sending back only the requested values. The comment should be updated.

IActionResult? mutationResult = null;
try
{
mutationResult = await mutationEngine.ExecuteAsync(context).ConfigureAwait(false);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ExecuteAsync is specifically creating the response as expected in a Rest Request. We should understand it is an MCP response and craft it as per the MCP endpoint needs instead of deserializing and serializing again.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use the SqlResponseHelper to create a response that MCP endpoint needs to avoid the duplicate serialization and deserialization.

{
Content = new List<ContentBlock>
{
new TextContentBlock { Type = "text", Text = output }
Copy link
Collaborator

@Aniruddh25 Aniruddh25 Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the tool call result be of Type text or json?

Copy link
Collaborator

@Aniruddh25 Aniruddh25 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments can be addressed as a followup - e.g. testing with authenticated, using SqlResponseHelper to create the McpResponse.

But some need to be addressed now - e.g. what are the expected output fields? only keys or all the fields?

@github-project-automation github-project-automation bot moved this from Todo to Review In Progress in Data API builder Oct 9, 2025
IServiceProvider serviceProvider,
CancellationToken cancellationToken = default)
{
ILogger<UpdateRecordTool>? logger = serviceProvider.GetService<ILogger<UpdateRecordTool>>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we ensure the configuration check to enable the tool is propagated to whether we expose the tool or not?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Review In Progress
Development

Successfully merging this pull request may close these issues.

[Enh]: Add DML tool: update_record
3 participants