Skip to content

Conversation

RubenCerna2079
Copy link
Contributor

Why make this change?

What is this change?

  • Create the read_records tool:
    • First it ensures that the MCP has the proper authorization to run the query
    • Then it creates the context with the parameters that were requested by the user
    • Lastly it calls the SqlQueryEngine in order to create and run the query and receive the results
  • The GenerateOrderByLists function inside the RequestParser.cs file was changed from private to public in order to allow the read_records tool to also use it to generate the proper context for the query.
  • Some functions inside of the SqlResponseHelper.cs file were changed to check if the query request comes from the read_records tool. This is done in order to output the correct information, right now the REST requests can also return a nextLink object which gives the API link necessary to get values in the case that not all of them were shown. We want to do something similar with the read_records tool, however we only want to return the after object which is the parameter that allows the query to know the exact place where it left from the previous query. This gets rid of unecessary information that can be found in the nextLink object.

Exceptions thrown when:

  1. Entity is empty or null.
  2. Parameters are not of the correct type.
  3. Parameters are not correctly written.
  4. Values inside orderby parameter are empty or null. (Note: orderby is an optional value as a whole, but the individual values it contains need to exist)
  5. Not having necessary permissions.

Errors:

  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. UnexpectedError - Any other UnexpectedError.

How was this tested?

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

Sample Request(s)

{ Entity: Book }
{ Select: title,publisher_id }
{ First: 3 }
{ Orderby: ["publisher_id asc", "title desc"] }

@RubenCerna2079 RubenCerna2079 added this to the 1.7 milestone Oct 3, 2025
@RubenCerna2079 RubenCerna2079 self-assigned this Oct 3, 2025
@Copilot Copilot AI review requested due to automatic review settings October 3, 2025 00:27
@RubenCerna2079 RubenCerna2079 linked an issue Oct 3, 2025 that may be closed by this pull request
6 tasks
@RubenCerna2079
Copy link
Contributor Author

/azp run

Copy link

Azure Pipelines successfully started running 6 pipeline(s).

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 read_records tool for the MCP (Model Context Protocol), enabling reading of entity records with parameters like select, filter, first, orderby, and after for pagination. The implementation includes proper authorization checks and handles both REST and MCP endpoint responses differently.

  • Added a new ReadRecordsTool class that implements the MCP read_records functionality
  • Modified response formatting to support both REST (nextLink) and MCP (after) pagination approaches
  • Changed GenerateOrderByLists method visibility from private to public for reuse

Reviewed Changes

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

File Description
src/Azure.DataApiBuilder.Mcp/BuiltInTools/ReadRecordsTool.cs Complete implementation of the read_records tool with argument parsing, authorization, and query execution
src/Core/Resolvers/SqlResponseHelpers.cs Enhanced response formatting to support both REST nextLink and MCP after pagination patterns
src/Core/Parsers/RequestParser.cs Changed GenerateOrderByLists method from private to public for reuse
src/Core/Parsers/FilterParser.cs Removed blank line formatting change

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

@RubenCerna2079
Copy link
Contributor Author

/azp run

Copy link

Azure Pipelines successfully started running 6 pipeline(s).

},
""filter"": {
""type"": ""string"",
""description"": ""A filter expression string to restrict results. Optional.""
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's update this to be more expressive:

A case-insensitive OData-like expression that defines a query predicate. Supports logical grouping with parentheses and the operators eq, ne, gt, ge, lt, le, and, or, not. Examples: year ge 1990, date lt 2025-01-01T00:00:00Z, (title eq 'Foundation') and (available ne false).

""properties"": {
""entity"": {
""type"": ""string"",
""description"": ""The entity name to read from. Required.""
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's update this to be more expressive:

The name of the entity to read, as provided by the describe_entities tool. Required.

},
""select"": {
""type"": ""string"",
""description"": ""A CSV of field names to include in the response. If not provided, all fields are returned. Optional.""
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's update this to be more expressive:

A comma-separated list of field names to include in the response. If omitted, all fields are returned. Optional.

return new Tool
{
Name = "read_records",
Description = "Reads the records from the specified entity.",
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's update this to be more expressive:

Retrieves records from a given entity.

},
""first"": {
""type"": ""integer"",
""description"": ""The maximum number of records to return in this page. Optional.""
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's update this to be more expressive:

The maximum number of records to return in the current page. Optional.

}
}

error = "You do not have permission to read records for this entity.";
Copy link
Contributor

Choose a reason for hiding this comment

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

You know the entity name so include it in the error message. Remember, error messages are intended to inform the caller with as much information as possible without leaking security so that the caller can correct the problem.

entityName


string output = JsonSerializer.Serialize(normalized, new JsonSerializerOptions { WriteIndented = true });

logger?.LogInformation("ReadRecordsTool success for entity {Entity}.", entityName);
Copy link
Contributor

Choose a reason for hiding this comment

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

Logs are nice, but where is the OTEL wrapper? This is important.

mcp_read_total_count, mcp_read_active_count, etc.

We want to ensure the developer experience across all 3 endpoints is the same.

{
Dictionary<string, object?> errorObj = new()
{
["status"] = "error",
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious. Why are we not setting Success = true and Success = false?

// Create a 'nextLink' object if the request comes from REST endpoint.
else
{
string basePaginationUri = SqlPaginationUtil.ConstructBaseUriForPagination(httpContext, runtimeConfig.Runtime?.BaseRoute);
Copy link
Contributor

Choose a reason for hiding this comment

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

I cannot tell by looking, you might just dismiss this comment, but does this line obey this?

{
  "runtime": {
    "rest": {
      "next-link-relative": true // default is false
    }
  }
}

{
public class ReadRecordsTool : IMcpTool
{
// private readonly IMetadataProviderFactory _metadataProviderFactory;
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove these lines since they are no longer used.

{
ILogger<ReadRecordsTool>? logger = serviceProvider.GetService<ILogger<ReadRecordsTool>>();

if (arguments == null)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Maybe we can move this inside try as well.

fieldsReturnedForFind = authResolver.GetAllowedExposedColumns(context.EntityName, effectiveRole!, context.OperationType);
}

context.UpdateReturnFields(fieldsReturnedForFind);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: we can add a comment on what this call is doing.


// Normalize response
string rawPayloadJson = ExtractResultJson(actionResult);
using JsonDocument result = JsonDocument.Parse(rawPayloadJson);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Can we declare this using in the imports section, start of the document?

}
}

private static bool TryResolveAuthorizedRole(
Copy link
Contributor

Choose a reason for hiding this comment

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

Add the function summary.

error = "You do not have permission to read records for this entity.";
return false;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Add the function summary.

}
};
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Add the function summary

public FilterClause GetFilterClause(string filterQueryString, string resourcePath, ODataUriResolver? customResolver = null)
{
if (_model is null)
{
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Is this new line introduced on purpose, if not we can revert it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not following, I deleted the line that shouldn't be there. I did not introduce a new line.

@github-project-automation github-project-automation bot moved this from Todo to Review In Progress in Data API builder Oct 7, 2025
@anushakolan
Copy link
Contributor

nit: Fix the description, Manual testing via MCP Inspecto

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

Successfully merging this pull request may close these issues.

[Enh]: Add DML tool: read_records
3 participants