diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000..6f711721f7
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,19 @@
+# Exclude repo metadata and local build outputs from Docker context
+.git/
+.vs/
+.vscode/
+TestResults/
+
+# Dotnet build artifacts
+**/bin/
+**/obj/
+**/out/
+
+# IDE and tooling miscellany
+*.user
+*.suo
+*.swp
+.DS_Store
+
+# Local docker artifacts
+Dockerfile.*.local
diff --git a/CODEOWNERS b/CODEOWNERS
index ed2b4835ef..ac65d7bc52 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1,7 +1,7 @@
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
# review when someone opens a pull request.
-* @Aniruddh25 @aaronburtle @anushakolan @RubenCerna2079 @souvikghosh04 @ravishetye @neeraj-sharma2592 @sourabh1007 @vadeveka @Alekhya-Polavarapu @rusamant
+* @Aniruddh25 @aaronburtle @anushakolan @RubenCerna2079 @souvikghosh04 @neeraj-sharma2592 @sourabh1007 @vadeveka @Alekhya-Polavarapu @rusamant @stuartpa
code_of_conduct.md @jerrynixon
contributing.md @jerrynixon
diff --git a/Dockerfile b/Dockerfile
index d6d950733c..1387981858 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,15 +1,34 @@
-# Version values referenced from https://hub.docker.com/_/microsoft-dotnet-aspnet
-
-FROM mcr.microsoft.com/dotnet/sdk:8.0-cbl-mariner2.0. AS build
+# Stage image versions mirror https://hub.docker.com/_/microsoft-dotnet-aspnet
+FROM mcr.microsoft.com/dotnet/sdk:8.0-cbl-mariner2.0 AS build
WORKDIR /src
-COPY [".", "./"]
-RUN dotnet build "./src/Service/Azure.DataApiBuilder.Service.csproj" -c Docker -o /out -r linux-x64
+
+# Copy project files first to maximize layer caching during restore
+COPY ["Nuget.config", "."]
+COPY ["global.json", "."]
+COPY ["src/Directory.Build.props", "src/"]
+COPY ["src/Directory.Packages.props", "src/"]
+COPY ["src/Azure.DataApiBuilder.sln", "src/"]
+COPY ["src/Service/Azure.DataApiBuilder.Service.csproj", "src/Service/"]
+COPY ["src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj", "src/Azure.DataApiBuilder.Mcp/"]
+COPY ["src/Core/Azure.DataApiBuilder.Core.csproj", "src/Core/"]
+COPY ["src/Auth/Azure.DataApiBuilder.Auth.csproj", "src/Auth/"]
+COPY ["src/Config/Azure.DataApiBuilder.Config.csproj", "src/Config/"]
+COPY ["src/Product/Azure.DataApiBuilder.Product.csproj", "src/Product/"]
+COPY ["src/Service.GraphQLBuilder/Azure.DataApiBuilder.Service.GraphQLBuilder.csproj", "src/Service.GraphQLBuilder/"]
+RUN dotnet restore "src/Service/Azure.DataApiBuilder.Service.csproj" -r linux-x64
+
+# Copy the remaining source and publish the service
+COPY . .
+RUN dotnet publish "src/Service/Azure.DataApiBuilder.Service.csproj" -c Release -f net8.0 -o /app/publish --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:8.0-cbl-mariner2.0 AS runtime
-COPY --from=build /out /App
-WORKDIR /App
+WORKDIR /app
+COPY --from=build /app/publish .
+
ENV ASPNETCORE_URLS=http://+:5000
+EXPOSE 5000
+
ENTRYPOINT ["dotnet", "Azure.DataApiBuilder.Service.dll"]
diff --git a/README.md b/README.md
index 1746a62482..d8edb22a50 100644
--- a/README.md
+++ b/README.md
@@ -7,9 +7,9 @@
[What's new?](https://learn.microsoft.com/azure/data-api-builder/whats-new)
-## Community
+## Join the community
-Join the Data API builder community! This sign up will help us maintain a list of interested developers to be part of our roadmap and to help us better understand the different ways DAB is being used. Sign up [here](https://forms.office.com/pages/responsepage.aspx?id=v4j5cvGGr0GRqy180BHbR1S1JdzGAxhDrefV-tBYtwZUNE1RWVo0SUVMTkRESUZLMVVOS0wwUFNVRy4u).
+Want to be part of our priorities and roadmap? Sign up [here](https://forms.office.com/pages/responsepage.aspx?id=v4j5cvGGr0GRqy180BHbR1S1JdzGAxhDrefV-tBYtwZUNE1RWVo0SUVMTkRESUZLMVVOS0wwUFNVRy4u).

@@ -17,199 +17,299 @@ Join the Data API builder community! This sign up will help us maintain a list o
Data API builder (DAB) is an open-source, no-code tool that creates secure, full-featured REST and GraphQL endpoints for your database. It’s a CRUD data API engine that runs in a container—on Azure, any other cloud, or on-premises. DAB is built for developers with integrated tooling, telemetry, and other productivity features.
-```mermaid
-erDiagram
- DATA_API_BUILDER ||--|{ DATA_API : "Provides"
- DATA_API_BUILDER {
- container true "Microsoft Container Repository"
- open-source true "MIT license / any cloud or on-prem."
- objects true "Supports: Table / View / Stored Procedure"
- developer true "Swagger / Nitro (fka Banana Cake Pop)"
- otel true "Open Telemetry / Structured Logs / Health Endpoints"
- security true "EntraId / EasyAuth / OAuth / JWT / Anonymous"
- cache true "Level1 (in-memory) / Level2 (redis)"
- policy true "Item policy / Database policy / Claims policy"
- hot_reload true "Dynamically controllable log levels"
- }
- DATA_API ||--o{ DATASOURCE : "Queries"
- DATA_API {
- REST true "$select / $filter / $orderby"
- GraphQL true "relationships / multiple mutations"
- }
- DATASOURCE {
- MS_SQL Supported
- PostgreSQL Supported
- Cosmos_DB Supported
- MySQL Supported
- SQL_DW Supported
- }
- CLIENT ||--o{ DATA_API : "Consumes"
- CLIENT {
- Transport HTTP "HTTP / HTTPS"
- Syntax JSON "Standard payloads"
- Mobile Supported "No requirement"
- Web Supported "No requirement"
- Desktop Supported "No requirement"
- Language Any "No requirement"
- Framework None "Not required"
- Library None "Not required"
- ORM None "Not required"
- Driver None "Not required"
- }
-```
+> [!IMPORTANT]
+> Data API builder (DAB) is open source and always free.
+
+### Which databases does Data API builder support?
+
+| | Azure SQL | SQL Server | SQLDW | Cosmos DB | PostgreSQL | MySQL |
+| :-----------: | :-------: | :--------: | :---: | :-------: | :--------: | :---: |
+| **Supported** | Yes | Yes | Yes | Yes | Yes | Yes |
+
+### Which environments does Data API builder support?
+
+| | On-Prem | Azure | AWS | GCP | Other |
+| :-----------: | :-----: | :---: | :--: | :--: | :---: |
+| **Supported** | Yes | Yes | Yes | Yes | Yes |
+
+### Which endpoints does Data API builder support?
+
+| | REST | GraphQL | MCP |
+| :-----------: | :--: | :-----: | :---------: |
+| **Supported** | Yes | Yes | Coming soon |
+
+## Getting started
+
+Use the [Getting Started](https://learn.microsoft.com/azure/data-api-builder/get-started/get-started-with-data-api-builder) tutorial to quickly explore the core tools and concepts.
-## Getting Started
+### 1. Install the `dotnet` [command line](https://get.dot.net)
-Use the [Getting Started](https://learn.microsoft.com/azure/data-api-builder/get-started/get-started-with-data-api-builder) tutorial to quickly explore the core tools and concepts. It gives you hands-on experience with how DAB makes you more efficient by removing boilerplate code.
+https://get.dot.net
-**1. Install the DAB CLI**
+> [!NOTE]
+> You may already have .NET installed!
-The [DAB CLI](https://aka.ms/dab/docs) is a cross-platform .NET tool. Install the [.NET SDK](https://get.dot.net) before running:
+The Data API builder (DAB) command line requires the .NET runtime version 8 or later.
+#### Validate your installation
+
+```sh
+dotnet --version
```
+
+### 2. Install the `dab` command line
+
+The Data API builder (DAB) command line is cross-platform and intended for local developer use.
+
+```sh
dotnet tool install microsoft.dataapibuilder -g
```
-**2. Create your initial configuration file**
+#### Validate your installation
+
+```sh
+dab --version
+```
+
+### 3. Create your database (example: Azure SQL database / T-SQL)
+
+This example uses a single table for simplicity.
+
+```sql
+CREATE TABLE dbo.Todo
+(
+ Id INT PRIMARY KEY IDENTITY,
+ Title NVARCHAR(500) NOT NULL,
+ IsCompleted BIT NOT NULL DEFAULT 0
+);
+INSERT dbo.Todo (Title, IsCompleted)
+VALUES
+ ('Walk the dog', 0),
+ ('Feed the fish', 0),
+ ('Clean the cat', 1);
+```
+
+### 4. Prepare your connection string
-DAB requires a JSON configuration file. Edit manually or with the CLI. Use `dab --help` for syntax options.
+Data API builder (DAB) supports `.env` files for testing process-level environment variables.
+#### PowerShell (Windows)
+
+```ps
+echo "my-connection-string=$env:database_connection_string" > .env
```
+
+#### cmd.exe (Windows)
+
+```cmd
+echo my-connection-string=%database_connection_string% > .env
+```
+
+#### bash (macOS/Linux)
+
+```bash
+echo "my-connection-string=$database_connection_string" > .env
+```
+
+#### Resulting .env file
+
+The file `.env` is automatically created through this process. These are the resulting contents:
+
+```
+"my-connection-string=$env:database_connection_string"
+```
+> [!NOTE]
+> Be sure and replace `database_connection_string` with your actual database connection string.
+
+> [!IMPORTANT]
+> Adding `.env` to your `.gitignore` file will help ensure your secrets are not added to source control.
+
+### 5. Create your initial configuration file
+
+Data API builder (DAB) requires a JSON configuration file. Use `dab --help` for syntax options.
+
+```sh
dab init
--database-type mssql
--connection-string "@env('my-connection-string')"
--host-mode development
```
-**3. Add your first table**
+> [!NOTE]
+> Including `--host-mode development` enables Swagger for REST and Nitro for GraphQL.
-DAB supports tables, views, and stored procedures. It works with SQL Server, Azure Cosmos DB, PostgreSQL, MySQL, and SQL Data Warehouse. Security is engine-level, but permissions are per entity.
+#### Resulting configuration
+The file `dab-config.json` is automatically created through this process. These are the resulting contents:
+
+```json
+{
+ "$schema": "https://github.com/Azure/data-api-builder/releases/download/v1.5.56/dab.draft.schema.json",
+ "data-source": {
+ "database-type": "mssql",
+ "connection-string": "@env('my-connection-string')",
+ "options": {
+ "set-session-context": false
+ }
+ },
+ "runtime": {
+ "rest": {
+ "enabled": true,
+ "path": "/api",
+ "request-body-strict": true
+ },
+ "graphql": {
+ "enabled": true,
+ "path": "/graphql",
+ "allow-introspection": true
+ },
+ "host": {
+ "cors": {
+ "origins": [],
+ "allow-credentials": false
+ },
+ "authentication": {
+ "provider": "StaticWebApps"
+ },
+ "mode": "development"
+ }
+ },
+ "entities": { }
+}
```
-dab add Actor
- --source "dbo.Actor"
+### 6. Add your table to the configuration
+
+```sh
+dab add Todo
+ --source "dbo.Todo"
--permissions "anonymous:*"
```
-**4. Run Data API builder**
+> [!NOTE]
+> DAB supports tables, views, and stored procedures. When the type is not specified, the default is `table`.
+
+#### Resulting configuration
+
+The `entities` section of the configuration is no longer empty:
+
+```json
+{
+ "entities": {
+ "Todo": {
+ "source": {
+ "object": "dbo.Todo",
+ "type": "table"
+ },
+ "graphql": {
+ "enabled": true,
+ "type": {
+ "singular": "Todo",
+ "plural": "Todos"
+ }
+ },
+ "rest": {
+ "enabled": true
+ },
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [
+ {
+ "action": "*"
+ }
+ ]
+ }
+ ]
+ }
+ }
+}
+```
-In `production`, DAB runs in a container. In `development`, it’s self-hosted locally with hot reload, Swagger, and Nitro (fka Banana Cake Pop) support.
+### 7. Run Data API builder
-```
+In `production`, DAB runs in a container. In `development`, it’s locally self-hosted.
+
+```sh
dab start
```
-> **Note**: Before you run `dab start`, make sure your connection string is stored in an environment variable called `my-connection-string`. This is required for `@env('my-connection-string')` in your config file to work. The easiest way is to create a `.env` file with `name=value` pairs—DAB will load these automatically at runtime.
+> [!IMPORTANT]
+> The DAB CLI assumes your configuration file is called `dab-config.json` and is in the local folder.
-**5. Access your data source**
+### 8. Access your data!
-By default, DAB enables both REST and GraphQL. REST supports `$select`, `$filter`, and `$orderBy`. GraphQL uses config-defined relationships.
+By default, DAB enables both REST and GraphQL.
+```sh
+GET http://localhost:5000/api/Todo
```
-GET http://localhost:5000/api/Actor
-```
-
-### Walk-through video
-
-
-
+> [!NOTE]
+> Change the URL to match your port if it is different.
-Demo source code: [startrek](https://aka.ms/dab/startrek)
+#### Other things you should try
-## Overview
-
-| Category | Features |
-|----------------|----------|
-| **Database Objects** | • NoSQL collections
• RDBMS tables, views, stored procedures |
-| **Data Sources** | • SQL Server & Azure SQL
• Azure Cosmos DB
• PostgreSQL
• MySQL |
-| **REST** | • `$select` for projection
• `$filter` for filtering
• `$orderBy` for sorting |
-| **GraphQL** | • Relationship navigation
• Data aggregation
• Multiple mutations |
-| **Telemetry** | • Structured logs
• OpenTelemetry
• Application Insights
• Health endpoints |
-| **Advanced** | • Pagination
• Level 1 (in-memory) cache |
-| **Authentication** | • OAuth2/JWT
• EasyAuth
• Entra ID |
-| **Authorization** | • Role-based support
• Entity permissions
• Database policies |
-| **Developer** | • Cross-platform CLI
• Swagger (REST)
• Nitro [previously Banana Cake Pop] (GraphQL)
• Open Source
• Configuration Hot Reload |
+* DAB’s Health endpoint: `http://localhost:5000/health`
+* DAB’s Swagger UI: `http://localhost:5000/swagger`
+* DAB’s Nitro UI: `http://localhost:5000/graphql`
## How does it work?
-This diagram shows how DAB works. DAB dynamically creates endpoints from your config file. It translates HTTP requests to SQL, returns JSON, and auto-pages results.
+DAB dynamically creates endpoints and translates requests to SQL, returning JSON.
```mermaid
sequenceDiagram
- actor Client
-
- box Data API builder (DAB)
- participant Endpoint
- participant QueryBuilder
- end
-
- participant Configuration as Configuration File
-
- box Data Source
- participant DB
- end
-
- Endpoint->>Endpoint: Start
- activate Endpoint
- Endpoint->>Configuration: Request
- Configuration-->>Endpoint: Configuration
- Endpoint->>DB: Request
- DB-->>Endpoint: Metadata
- Note over Endpoint, DB: Some configuration is validated against the metadata
- Endpoint-->>Endpoint: Configure
- deactivate Endpoint
- Client-->>Endpoint: HTTP Request
- activate Endpoint
- critical
- Endpoint-->>Endpoint: Authenticate
- Endpoint-->>Endpoint: Authorize
- end
- Endpoint->>QueryBuilder: Request
- QueryBuilder-->>Endpoint: SQL
- alt Cache
- Endpoint-->>Endpoint: Use Cache
- else Query
- Endpoint-->>DB: Request
- Note over Endpoint, DB: Query is automatically throttled and results paginated
- DB->>Endpoint: Results
- Note over Endpoint, DB: Results are automatically cached for use in next request
- end
- Endpoint->>Client: HTTP 200
- deactivate Endpoint
-```
-
-Because DAB is stateless, it can scale up or out using any container size. It builds a feature-rich API like you would from scratch—but now you don’t have to.
-
-## Additional Resources
-
-- [Online Documentation](https://aka.ms/dab/docs)
-- [Official Samples](https://aka.ms/dab/samples)
-- [Known Issues](https://learn.microsoft.com/azure/data-api-builder/known-issues)
-- [Feature Roadmap](https://github.com/Azure/data-api-builder/discussions/1377)
+ actor Client as Client
+ participant Endpoint as Endpoint
+ participant QueryBuilder as QueryBuilder
+ participant DB as Database
+
+ %% Initialization / Warming up section (light grey)
+ rect rgba(120,120,120,0.10)
+ Endpoint -->>+ Endpoint: Read Config
+ Endpoint ->> DB: Query Metadata
+ DB -->> Endpoint: Metadata Response
+ Endpoint ->>- Endpoint: Start Engine
+ end
+
+ %% Request/Response section (very light purple)
+ rect rgba(180,150,255,0.11)
+ Client ->>+ Endpoint: HTTP Request
+ Endpoint ->> Endpoint: Authorize
+ Endpoint ->> QueryBuilder: Invoke
+ QueryBuilder -->> Endpoint: SQL Query
+ Endpoint ->> DB: Submit Query
+ DB -->> Endpoint: Data Response
+ Endpoint -->>- Client: HTTP Response
+ end
+```
-#### References
+## Additional resources
-- [Microsoft REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md)
-- [Microsoft Azure REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md)
-- [GraphQL Specification](https://graphql.org/)
+* [Online Documentation](https://aka.ms/dab/docs)
+* [Official Samples](https://aka.ms/dab/samples)
+* [Known Issues](https://learn.microsoft.com/azure/data-api-builder/known-issues)
+* [Feature Roadmap](https://github.com/Azure/data-api-builder/discussions/1377)
-### How to Contribute
+#### References
-To contribute, see these documents:
+* [Microsoft REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md)
+* [Microsoft Azure REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md)
+* [GraphQL Specification](https://graphql.org/)
-- [Code of Conduct](./CODE_OF_CONDUCT.md)
-- [Security](./SECURITY.md)
-- [Contributing](./CONTRIBUTING.md)
+### How to contribute
-### License
+To contribute, see these documents:
-**Data API builder for Azure Databases** is licensed under the MIT License. See [LICENSE](./LICENSE.txt) for details.
+* [Code of Conduct](./CODE_OF_CONDUCT.md)
+* [Security](./SECURITY.md)
+* [Contributing](./CONTRIBUTING.md)
+* [MIT License](./LICENSE.txt)
-### Third-Party Component Notice
+### Third-party component notice
-Nitro (fka Banana Cake Pop by ChilliCream, Inc.) may optionally store work in its cloud service via your ChilliCream account. Microsoft is not affiliated with or endorsing this service. Use at your discretion.
+Nitro (formerly Banana Cake Pop by ChilliCream, Inc.) may optionally store work in its cloud service via your ChilliCream account. Microsoft is not affiliated with or endorsing this service. Use at your discretion.
### Trademarks
-This project may use trademarks or logos. Use of Microsoft trademarks must follow Microsoft’s [Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks). Use of third-party marks is subject to their policies.
+This project may use trademarks or logos. Use of Microsoft trademarks must follow Microsoft’s [Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks). Use of third-party marks is subject to their policies.
\ No newline at end of file
diff --git a/config-generators/dwsql-commands.txt b/config-generators/dwsql-commands.txt
index 1f613d7f48..df4940ae59 100644
--- a/config-generators/dwsql-commands.txt
+++ b/config-generators/dwsql-commands.txt
@@ -57,6 +57,7 @@ update Publisher --config "dab-config.DwSql.json" --permissions "policy_tester_0
update Publisher --config "dab-config.DwSql.json" --permissions "policy_tester_06:read" --fields.include "*" --policy-database "@item.id eq 1940"
update Publisher --config "dab-config.DwSql.json" --permissions "database_policy_tester:read" --policy-database "@item.id ne 1234 or @item.id gt 1940"
update Publisher --config "dab-config.DwSql.json" --permissions "database_policy_tester:update" --policy-database "@item.id ne 1234"
+update Publisher --config "dab-config.DwSql.json" --permissions "database_policy_tester:create" --policy-database "@item.name ne 'New publisher'"
update Stock --config "dab-config.DwSql.json" --permissions "authenticated:create,read,update,delete"
update Stock --config "dab-config.DwSql.json" --rest commodities --graphql true
update Stock --config "dab-config.DwSql.json" --permissions "TestNestedFilterFieldIsNull_ColumnForbidden:read"
diff --git a/config-generators/mssql-commands.txt b/config-generators/mssql-commands.txt
index 158d4bd179..cecc6b522c 100644
--- a/config-generators/mssql-commands.txt
+++ b/config-generators/mssql-commands.txt
@@ -207,6 +207,7 @@ update BookNF --config "dab-config.MsSql.json" --permissions "TestNestedFilter_E
update BookNF --config "dab-config.MsSql.json" --permissions "TestNestedFilter_ColumnForbidden:read"
update BookNF --config "dab-config.MsSql.json" --permissions "TestNestedFilterChained_EntityReadForbidden:read"
update BookNF --config "dab-config.MsSql.json" --permissions "TestNestedFilterChained_ColumnForbidden:read"
+update BookNF --config "dab-config.MsSql.json" --permissions "TestFieldExcludedForAggregation:read" --fields.exclude "publisher_id"
update BookNF --config "dab-config.MsSql.json" --relationship publishers --target.entity PublisherNF --cardinality one
update BookNF --config "dab-config.MsSql.json" --relationship websiteplacement --target.entity BookWebsitePlacement --cardinality one
update BookNF --config "dab-config.MsSql.json" --relationship reviews --target.entity Review --cardinality many
@@ -235,3 +236,4 @@ add dbo_DimAccount --config "dab-config.MsSql.json" --source "DimAccount" --perm
update dbo_DimAccount --config "dab-config.MsSql.json" --relationship parent_account --target.entity dbo_DimAccount --cardinality one --relationship.fields "ParentAccountKey:AccountKey"
update dbo_DimAccount --config "dab-config.MsSql.json" --relationship child_accounts --target.entity dbo_DimAccount --cardinality many --relationship.fields "AccountKey:ParentAccountKey"
add DateOnlyTable --config "dab-config.MsSql.json" --source "date_only_table" --permissions "anonymous:*" --rest true --graphql true --source.key-fields "event_date"
+add GetBooksAuth --config "dab-config.MsSql.json" --source "get_books" --source.type "stored-procedure" --permissions "teststoredprocauth:execute" --rest true --graphql true --graphql.operation "Query" --rest.methods "Get"
diff --git a/docs/Testing/mcp-inspector-testing.md b/docs/Testing/mcp-inspector-testing.md
new file mode 100644
index 0000000000..d6942311ea
--- /dev/null
+++ b/docs/Testing/mcp-inspector-testing.md
@@ -0,0 +1,23 @@
+
+# MCP Inspector Testing Guide
+
+Steps to run and test MCP tools using the https://www.npmjs.com/package/@modelcontextprotocol/inspector.
+### Pre-requisite:
+- Node.js must be installed on your system to run this code.
+- Ensure that the DAB MCP server is running before attempting to connect with the inspector tool.
+
+### 1. **Install MCP Inspector**
+npx @modelcontextprotocol/inspector
+
+### 2. ** Bypass TLS Verification (For Local Testing)**
+set NODE_TLS_REJECT_UNAUTHORIZED=0
+
+### 3. ** Open the inspector with pre-filled token.**
+http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=
+
+### 4. ** How to use the tool..**
+- Set the transport type "Streamable HTTP".
+- Set the URL "http://localhost:5000/mcp" and hit connect.
+- Select a Tool from the dropdown list.
+- Fill in the Parameters required for the tool.
+- Click "Run" to execute the tool and view the response.
\ No newline at end of file
diff --git a/global.json b/global.json
index 391ba3c2a3..7e9f2f6bb4 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "8.0.100",
+ "version": "8.0.414",
"rollForward": "latestFeature"
}
}
diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json
index 20903284b0..80cfd953ad 100644
--- a/schemas/dab.draft.schema.json
+++ b/schemas/dab.draft.schema.json
@@ -158,16 +158,21 @@
"type": "object",
"properties": {
"max-page-size": {
- "type": ["integer", "null"],
+ "type": [ "integer", "null" ],
"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.",
"default": 100000,
"minimum": 1
},
"default-page-size": {
- "type": ["integer", "null"],
+ "type": [ "integer", "null" ],
"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.",
"default": 100,
"minimum": 1
+ },
+ "next-link-relative": {
+ "type": "boolean",
+ "default": false,
+ "description": "When true, nextLink in paginated results will use a relative URL."
}
}
},
@@ -209,7 +214,7 @@
"description": "Allow enabling/disabling GraphQL requests for all entities."
},
"depth-limit": {
- "type": ["integer", "null"],
+ "type": [ "integer", "null" ],
"description": "Maximum allowed depth of a GraphQL query.",
"default": null
},
@@ -234,13 +239,74 @@
}
}
},
+ "mcp": {
+ "type": "object",
+ "description": "Global MCP endpoint configuration",
+ "additionalProperties": false,
+ "properties": {
+ "path": {
+ "default": "/mcp",
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean",
+ "description": "Allow enabling/disabling MCP requests for all entities.",
+ "default": true
+ },
+ "dml-tools": {
+ "oneOf": [
+ {
+ "type": "boolean",
+ "description": "Enable/disable all DML tools with default settings."
+ },
+ {
+ "type": "object",
+ "description": "Individual DML tools configuration",
+ "additionalProperties": false,
+ "properties": {
+ "describe-entities": {
+ "type": "boolean",
+ "description": "Enable/disable the describe-entities tool.",
+ "default": false
+ },
+ "create-record": {
+ "type": "boolean",
+ "description": "Enable/disable the create-record tool.",
+ "default": false
+ },
+ "read-records": {
+ "type": "boolean",
+ "description": "Enable/disable the read-records tool.",
+ "default": false
+ },
+ "update-record": {
+ "type": "boolean",
+ "description": "Enable/disable the update-record tool.",
+ "default": false
+ },
+ "delete-record": {
+ "type": "boolean",
+ "description": "Enable/disable the delete-record tool.",
+ "default": false
+ },
+ "execute-entity": {
+ "type": "boolean",
+ "description": "Enable/disable the execute-entity tool.",
+ "default": false
+ }
+ }
+ }
+ ]
+ }
+ }
+ },
"host": {
"type": "object",
"description": "Global hosting configuration",
"additionalProperties": false,
"properties": {
"max-response-size-mb": {
- "type": ["integer", "null"],
+ "type": [ "integer", "null" ],
"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.",
"default": 158,
"minimum": 1,
@@ -248,12 +314,12 @@
},
"mode": {
"description": "Set if running in Development or Production mode",
- "type": ["string", "null"],
+ "type": [ "string", "null" ],
"default": "production",
- "enum": ["production", "development"]
+ "enum": [ "production", "development" ]
},
"cors": {
- "type": ["object", "null"],
+ "type": [ "object", "null" ],
"description": "Configure CORS",
"additionalProperties": false,
"properties": {
@@ -273,7 +339,7 @@
}
},
"authentication": {
- "type": ["object", "null"],
+ "type": [ "object", "null" ],
"additionalProperties": false,
"properties": {
"provider": {
@@ -317,7 +383,7 @@
"type": "string"
}
},
- "required": ["audience", "issuer"]
+ "required": [ "audience", "issuer" ]
}
},
"allOf": [
@@ -333,9 +399,9 @@
]
}
},
- "required": ["provider"]
+ "required": [ "provider" ]
},
- "then": { "required": ["jwt"] },
+ "then": { "required": [ "jwt" ] },
"else": { "properties": { "jwt": false } }
}
]
@@ -377,7 +443,7 @@
"default": true
}
},
- "required": ["connection-string"]
+ "required": [ "connection-string" ]
},
"open-telemetry": {
"type": "object",
@@ -400,7 +466,7 @@
"type": "string",
"description": "Open Telemetry protocol",
"default": "grpc",
- "enum": ["grpc", "httpprotobuf"]
+ "enum": [ "grpc", "httpprotobuf" ]
},
"enabled": {
"type": "boolean",
@@ -408,7 +474,119 @@
"default": true
}
},
- "required": ["endpoint"]
+ "required": [ "endpoint" ]
+ },
+ "azure-log-analytics": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "description": "Allow enabling/disabling Azure Log Analytics.",
+ "default": false
+ },
+ "auth": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "custom-table-name": {
+ "type": [ "string", "null" ],
+ "description": "Azure Log Analytics Custom Table Name for entra-id mode"
+ },
+ "dcr-immutable-id": {
+ "type": [ "string", "null" ],
+ "description": "DCR ID for entra-id mode"
+ },
+ "dce-endpoint": {
+ "type": [ "string", "null" ],
+ "description": "DCE endpoint for entra-id mode"
+ }
+ }
+ },
+ "dab-identifier": {
+ "type": "string",
+ "description": "Identifier passed on to Log Analytics",
+ "default": "DabLogs"
+ },
+ "flush-interval-seconds": {
+ "type": "integer",
+ "description": "Interval between log batch pushes (in seconds)",
+ "default": 5
+ }
+ },
+ "if": {
+ "properties": {
+ "enabled": {
+ "const": true
+ }
+ }
+ },
+ "then": {
+ "properties": {
+ "auth": {
+ "properties": {
+ "custom-table-name": {
+ "type": "string",
+ "description": "Azure Log Analytics Custom Table Name for entra-id mode"
+ },
+ "dcr-immutable-id": {
+ "type": "string",
+ "description": "DCR ID for entra-id mode"
+ },
+ "dce-endpoint": {
+ "type": "string",
+ "description": "DCE endpoint for entra-id mode"
+ }
+ },
+ "required": [ "custom-table-name", "dcr-immutable-id", "dce-endpoint" ]
+ }
+ },
+ "required": [ "auth" ]
+ }
+ },
+ "file": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "description": "Enable/disable file sink telemetry logging.",
+ "default": false
+ },
+ "path": {
+ "type": "string",
+ "description": "File path for telemetry logs.",
+ "default": "/logs/dab-log.txt"
+ },
+ "rolling-interval": {
+ "type": "string",
+ "description": "Rolling interval for log files.",
+ "default": "Day",
+ "enum": [ "Minute", "Hour", "Day", "Month", "Year", "Infinite" ]
+ },
+ "retained-file-count-limit": {
+ "type": "integer",
+ "description": "Maximum number of retained log files.",
+ "default": 1,
+ "minimum": 1
+ },
+ "file-size-limit-bytes": {
+ "type": "integer",
+ "description": "Maximum file size in bytes before rolling.",
+ "default": 1048576,
+ "minimum": 1
+ }
+ },
+ "if": {
+ "properties": {
+ "enabled": {
+ "const": true
+ }
+ }
+ },
+ "then": {
+ "required": [ "path" ]
+ }
},
"log-level": {
"type": "object",
@@ -457,6 +635,59 @@
"type": "integer",
"description": "Time to live in seconds for the Comprehensive Health Check Report cache entry.",
"default": 5
+ },
+ "max-query-parallelism": {
+ "type": "integer",
+ "description": "The max degree of parallelism for running parallel health check queries.",
+ "default": 4
+ }
+ }
+ }
+ }
+ },
+ "azure-key-vault": {
+ "type": "object",
+ "description": "Azure Key Vault configuration for storing secrets",
+ "additionalProperties": false,
+ "properties": {
+ "endpoint": {
+ "type": "string",
+ "description": "Azure Key Vault endpoint URL"
+ },
+ "retry-policy": {
+ "type": "object",
+ "description": "Retry policy configuration for Azure Key Vault operations",
+ "additionalProperties": false,
+ "properties": {
+ "mode": {
+ "type": "string",
+ "enum": ["fixed", "exponential"],
+ "default": "exponential",
+ "description": "Retry mode: fixed or exponential backoff"
+ },
+ "max-count": {
+ "type": "integer",
+ "default": 3,
+ "minimum": 0,
+ "description": "Maximum number of retry attempts"
+ },
+ "delay-seconds": {
+ "type": "integer",
+ "default": 1,
+ "minimum": 1,
+ "description": "Initial delay between retries in seconds"
+ },
+ "max-delay-seconds": {
+ "type": "integer",
+ "default": 60,
+ "minimum": 1,
+ "description": "Maximum delay between retries in seconds (for exponential mode)"
+ },
+ "network-timeout-seconds": {
+ "type": "integer",
+ "default": 60,
+ "minimum": 1,
+ "description": "Network timeout for requests in seconds"
}
}
}
@@ -470,6 +701,10 @@
"type": "object",
"additionalProperties": false,
"properties": {
+ "description": {
+ "type": "string",
+ "description": "Optional description for the entity. Will be surfaced in generated API documentation and GraphQL schema as comments."
+ },
"health": {
"description": "Health check configuration for entity",
"type": [ "object", "null" ],
@@ -520,23 +755,35 @@
"description": "Database object name"
},
"parameters": {
- "type": "object",
- "description": "Dictionary of parameters and their values",
- "patternProperties": {
- "^.*$": {
- "oneOf": [
- {
- "type": "boolean"
- },
- {
- "type": "string"
- },
- {
- "type": "number"
+ "oneOf": [
+ {
+ "type": "object",
+ "description": "Dictionary of parameters and their values (deprecated)",
+ "patternProperties": {
+ "^.*$": {
+ "oneOf": [
+ { "type": "boolean" },
+ { "type": "string" },
+ { "type": "number" }
+ ]
+ }
+ }
+ },
+ {
+ "type": "array",
+ "description": "Array of parameter objects with metadata",
+ "items": {
+ "type": "object",
+ "required": ["name"],
+ "properties": {
+ "name": { "type": "string", "description": "Parameter name" },
+ "required": { "type": "boolean", "description": "Is parameter required" },
+ "default": { "type": ["string", "number", "boolean", "null"], "description": "Default value" },
+ "description": { "type": "string", "description": "Parameter description. Since descriptions for multiple parameters are provided as a comma-separated string, individual parameter descriptions must not contain a comma (',')." }
}
- ]
+ }
}
- }
+ ]
},
"key-fields": {
"type": "array",
@@ -550,6 +797,21 @@
}
]
},
+ "fields": {
+ "type": "array",
+ "description": "Defines the fields (columns) exposed for this entity, with metadata.",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": { "type": "string", "description": "Database column name." },
+ "alias": { "type": "string", "description": "Exposed name for the field." },
+ "description": { "type": "string", "description": "Field description." },
+ "primary-key": { "type": "boolean", "description": "Indicates whether this field is a primary key." }
+ },
+ "required": ["name"]
+ },
+ "uniqueItems": true
+ },
"rest": {
"oneOf": [
{
@@ -869,11 +1131,36 @@
}
}
},
- "required": ["source", "permissions"]
+ "required": ["source", "permissions"],
+ "allOf": [
+ {
+ "if": {
+ "required": ["fields"]
+ },
+ "then": {
+ "not": {
+ "anyOf": [
+ { "required": ["mappings"] },
+ { "properties": { "source": { "properties": { "key-fields": { } }, "required": ["key-fields"] } } }
+ ]
+ }
+ }
+ }
+ ]
}
}
}
},
+ "if": {
+ "required": ["azure-key-vault"]
+ },
+ "then": {
+ "properties": {
+ "azure-key-vault": {
+ "required": ["endpoint"]
+ }
+ }
+ },
"required": ["data-source", "entities"],
"$defs": {
"singular-plural": {
diff --git a/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj b/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj
new file mode 100644
index 0000000000..f675f8d8d1
--- /dev/null
+++ b/src/Azure.DataApiBuilder.Mcp/Azure.DataApiBuilder.Mcp.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs
new file mode 100644
index 0000000000..6fbe08879b
--- /dev/null
+++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs
@@ -0,0 +1,275 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json;
+using Azure.DataApiBuilder.Auth;
+using Azure.DataApiBuilder.Config.DatabasePrimitives;
+using Azure.DataApiBuilder.Config.ObjectModel;
+using Azure.DataApiBuilder.Core.Authorization;
+using Azure.DataApiBuilder.Core.Configurations;
+using Azure.DataApiBuilder.Core.Models;
+using Azure.DataApiBuilder.Core.Resolvers;
+using Azure.DataApiBuilder.Core.Resolvers.Factories;
+using Azure.DataApiBuilder.Core.Services;
+using Azure.DataApiBuilder.Core.Services.MetadataProviders;
+using Azure.DataApiBuilder.Mcp.Model;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using ModelContextProtocol.Protocol;
+using static Azure.DataApiBuilder.Mcp.Model.McpEnums;
+
+namespace Azure.DataApiBuilder.Mcp.BuiltInTools
+{
+ public class CreateRecordTool : IMcpTool
+ {
+ public ToolType ToolType { get; } = ToolType.BuiltIn;
+
+ public Tool GetToolMetadata()
+ {
+ return new Tool
+ {
+ Name = "create_record",
+ Description = "Creates a new record in the specified entity.",
+ InputSchema = JsonSerializer.Deserialize(
+ @"{
+ ""type"": ""object"",
+ ""properties"": {
+ ""entity"": {
+ ""type"": ""string"",
+ ""description"": ""The name of the entity""
+ },
+ ""data"": {
+ ""type"": ""object"",
+ ""description"": ""The data for the new record""
+ }
+ },
+ ""required"": [""entity"", ""data""]
+ }"
+ )
+ };
+ }
+
+ public async Task ExecuteAsync(
+ JsonDocument? arguments,
+ IServiceProvider serviceProvider,
+ CancellationToken cancellationToken = default)
+ {
+ ILogger? logger = serviceProvider.GetService>();
+ if (arguments == null)
+ {
+ return Utils.McpResponseBuilder.BuildErrorResult("Invalid Arguments", "No arguments provided", logger);
+ }
+
+ RuntimeConfigProvider runtimeConfigProvider = serviceProvider.GetRequiredService();
+ if (!runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig))
+ {
+ return Utils.McpResponseBuilder.BuildErrorResult("Invalid Configuration", "Runtime configuration not available", logger);
+ }
+
+ if (runtimeConfig.McpDmlTools?.CreateRecord != true)
+ {
+ return Utils.McpResponseBuilder.BuildErrorResult(
+ "ToolDisabled",
+ "The create_record tool is disabled in the configuration.",
+ logger);
+ }
+
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ JsonElement root = arguments.RootElement;
+
+ if (!root.TryGetProperty("entity", out JsonElement entityElement) ||
+ !root.TryGetProperty("data", out JsonElement dataElement))
+ {
+ return Utils.McpResponseBuilder.BuildErrorResult("InvalidArguments", "Missing required arguments 'entity' or 'data'", logger);
+ }
+
+ string entityName = entityElement.GetString() ?? string.Empty;
+ if (string.IsNullOrWhiteSpace(entityName))
+ {
+ return Utils.McpResponseBuilder.BuildErrorResult("InvalidArguments", "Entity name cannot be empty", logger);
+ }
+
+ string dataSourceName;
+ try
+ {
+ dataSourceName = runtimeConfig.GetDataSourceNameFromEntityName(entityName);
+ }
+ catch (Exception)
+ {
+ return Utils.McpResponseBuilder.BuildErrorResult("InvalidConfiguration", $"Entity '{entityName}' not found in configuration", logger);
+ }
+
+ IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService();
+ ISqlMetadataProvider sqlMetadataProvider = metadataProviderFactory.GetMetadataProvider(dataSourceName);
+
+ DatabaseObject dbObject;
+ try
+ {
+ dbObject = sqlMetadataProvider.GetDatabaseObjectByKey(entityName);
+ }
+ catch (Exception)
+ {
+ return Utils.McpResponseBuilder.BuildErrorResult("InvalidConfiguration", $"Database object for entity '{entityName}' not found", logger);
+ }
+
+ // Create an HTTP context for authorization
+ IHttpContextAccessor httpContextAccessor = serviceProvider.GetRequiredService();
+ HttpContext httpContext = httpContextAccessor.HttpContext ?? new DefaultHttpContext();
+ IAuthorizationResolver authorizationResolver = serviceProvider.GetRequiredService();
+
+ if (httpContext is null || !authorizationResolver.IsValidRoleContext(httpContext))
+ {
+ return Utils.McpResponseBuilder.BuildErrorResult("PermissionDenied", "Permission denied: Unable to resolve a valid role context for update operation.", logger);
+ }
+
+ // Validate that we have at least one role authorized for create
+ if (!TryResolveAuthorizedRole(httpContext, authorizationResolver, entityName, out string authError))
+ {
+ return Utils.McpResponseBuilder.BuildErrorResult("PermissionDenied", authError, logger);
+ }
+
+ JsonElement insertPayloadRoot = dataElement.Clone();
+ InsertRequestContext insertRequestContext = new(
+ entityName,
+ dbObject,
+ insertPayloadRoot,
+ EntityActionOperation.Insert);
+
+ RequestValidator requestValidator = serviceProvider.GetRequiredService();
+
+ // Only validate tables
+ if (dbObject.SourceType is EntitySourceType.Table)
+ {
+ try
+ {
+ requestValidator.ValidateInsertRequestContext(insertRequestContext);
+ }
+ catch (Exception ex)
+ {
+ return Utils.McpResponseBuilder.BuildErrorResult("ValidationFailed", $"Request validation failed: {ex.Message}", logger);
+ }
+ }
+ else
+ {
+ return Utils.McpResponseBuilder.BuildErrorResult(
+ "InvalidCreateTarget",
+ "The create_record tool is only available for tables.",
+ logger);
+ }
+
+ IMutationEngineFactory mutationEngineFactory = serviceProvider.GetRequiredService();
+ DatabaseType databaseType = sqlMetadataProvider.GetDatabaseType();
+ IMutationEngine mutationEngine = mutationEngineFactory.GetMutationEngine(databaseType);
+
+ IActionResult? result = await mutationEngine.ExecuteAsync(insertRequestContext);
+
+ if (result is CreatedResult createdResult)
+ {
+ return Utils.McpResponseBuilder.BuildSuccessResult(
+ new Dictionary
+ {
+ ["entity"] = entityName,
+ ["result"] = createdResult.Value,
+ ["message"] = $"Successfully created record in entity '{entityName}'"
+ },
+ logger,
+ $"Successfully created record in entity '{entityName}'");
+ }
+ else if (result is ObjectResult objectResult)
+ {
+ bool isError = objectResult.StatusCode.HasValue && objectResult.StatusCode.Value >= 400 && objectResult.StatusCode.Value != 403;
+ if (isError)
+ {
+ return Utils.McpResponseBuilder.BuildErrorResult(
+ "CreateFailed",
+ $"Failed to create record in entity '{entityName}'. Error: {JsonSerializer.Serialize(objectResult.Value)}",
+ logger);
+ }
+ else
+ {
+ return Utils.McpResponseBuilder.BuildSuccessResult(
+ new Dictionary
+ {
+ ["entity"] = entityName,
+ ["result"] = objectResult.Value,
+ ["message"] = $"Successfully created record in entity '{entityName}'. Unable to perform read-back of inserted records."
+ },
+ logger,
+ $"Successfully created record in entity '{entityName}'. Unable to perform read-back of inserted records.");
+ }
+ }
+ else
+ {
+ if (result is null)
+ {
+ return Utils.McpResponseBuilder.BuildErrorResult(
+ "UnexpectedError",
+ $"Mutation engine returned null result for entity '{entityName}'",
+ logger);
+ }
+ else
+ {
+ return Utils.McpResponseBuilder.BuildSuccessResult(
+ new Dictionary
+ {
+ ["entity"] = entityName,
+ ["message"] = $"Create operation completed with unexpected result type: {result.GetType().Name}"
+ },
+ logger,
+ $"Create operation completed for entity '{entityName}' with unexpected result type: {result.GetType().Name}");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ return Utils.McpResponseBuilder.BuildErrorResult("Error", $"Error: {ex.Message}", logger);
+ }
+ }
+
+ private static bool TryResolveAuthorizedRole(
+ HttpContext httpContext,
+ IAuthorizationResolver authorizationResolver,
+ string entityName,
+ out string error)
+ {
+ error = string.Empty;
+
+ string roleHeader = httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER].ToString();
+
+ if (string.IsNullOrWhiteSpace(roleHeader))
+ {
+ error = "Client role header is missing or empty.";
+ return false;
+ }
+
+ string[] roles = roleHeader
+ .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
+ if (roles.Length == 0)
+ {
+ error = "Client role header is missing or empty.";
+ return false;
+ }
+
+ foreach (string role in roles)
+ {
+ bool allowed = authorizationResolver.AreRoleAndOperationDefinedForEntity(
+ entityName, role, EntityActionOperation.Create);
+
+ if (allowed)
+ {
+ return true;
+ }
+ }
+
+ error = "You do not have permission to create records for this entity.";
+ return false;
+ }
+ }
+}
diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs
new file mode 100644
index 0000000000..86a5ce15ec
--- /dev/null
+++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs
@@ -0,0 +1,346 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Data.Common;
+using System.Text.Json;
+using Azure.DataApiBuilder.Auth;
+using Azure.DataApiBuilder.Config.DatabasePrimitives;
+using Azure.DataApiBuilder.Config.ObjectModel;
+using Azure.DataApiBuilder.Core.Configurations;
+using Azure.DataApiBuilder.Core.Models;
+using Azure.DataApiBuilder.Core.Resolvers;
+using Azure.DataApiBuilder.Core.Resolvers.Factories;
+using Azure.DataApiBuilder.Core.Services;
+using Azure.DataApiBuilder.Core.Services.MetadataProviders;
+using Azure.DataApiBuilder.Mcp.Model;
+using Azure.DataApiBuilder.Mcp.Utils;
+using Azure.DataApiBuilder.Service.Exceptions;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Data.SqlClient;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using ModelContextProtocol.Protocol;
+using static Azure.DataApiBuilder.Mcp.Model.McpEnums;
+
+namespace Azure.DataApiBuilder.Mcp.BuiltInTools
+{
+ ///
+ /// Tool to delete records from a table/view entity configured in DAB.
+ /// Supports both simple and composite primary keys.
+ ///
+ public class DeleteRecordTool : IMcpTool
+ {
+ ///
+ /// Gets the type of the tool, which is BuiltIn for this implementation.
+ ///
+ public ToolType ToolType { get; } = ToolType.BuiltIn;
+
+ ///
+ /// Gets the metadata for the delete-record tool, including its name, description, and input schema.
+ ///
+ public Tool GetToolMetadata()
+ {
+ return new Tool
+ {
+ Name = "delete_record",
+ Description = "Deletes a record from a table based on primary key or composite key",
+ InputSchema = JsonSerializer.Deserialize(
+ @"{
+ ""type"": ""object"",
+ ""properties"": {
+ ""entity"": {
+ ""type"": ""string"",
+ ""description"": ""The name of the entity (table) as configured in dab-config. Required.""
+ },
+ ""keys"": {
+ ""type"": ""object"",
+ ""description"": ""Primary key values to identify the record to delete. For composite keys, provide all key columns as properties. Required.""
+ }
+ },
+ ""required"": [""entity"", ""keys""]
+ }"
+ )
+ };
+ }
+
+ ///
+ /// Executes the delete-record tool, deleting an existing record in the specified entity using provided keys.
+ ///
+ public async Task ExecuteAsync(
+ JsonDocument? arguments,
+ IServiceProvider serviceProvider,
+ CancellationToken cancellationToken = default)
+ {
+ ILogger? logger = serviceProvider.GetService>();
+
+ try
+ {
+ // Cancellation check at the start
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // 1) Resolve required services & configuration
+ RuntimeConfigProvider runtimeConfigProvider = serviceProvider.GetRequiredService();
+ RuntimeConfig config = runtimeConfigProvider.GetConfig();
+
+ // 2) Check if the tool is enabled in configuration before proceeding
+ if (config.McpDmlTools?.DeleteRecord != true)
+ {
+ return McpResponseBuilder.BuildErrorResult(
+ "ToolDisabled",
+ $"The {this.GetToolMetadata().Name} tool is disabled in the configuration.",
+ logger);
+ }
+
+ // 3) Parsing & basic argument validation
+ if (arguments is null)
+ {
+ return McpResponseBuilder.BuildErrorResult("InvalidArguments", "No arguments provided.", logger);
+ }
+
+ if (!McpArgumentParser.TryParseEntityAndKeys(arguments.RootElement, out string entityName, out Dictionary keys, out string parseError))
+ {
+ return McpResponseBuilder.BuildErrorResult("InvalidArguments", parseError, logger);
+ }
+
+ IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService();
+ IMutationEngineFactory mutationEngineFactory = serviceProvider.GetRequiredService();
+
+ // 4) Resolve metadata for entity existence check
+ string dataSourceName;
+ ISqlMetadataProvider sqlMetadataProvider;
+
+ try
+ {
+ dataSourceName = config.GetDataSourceNameFromEntityName(entityName);
+ sqlMetadataProvider = metadataProviderFactory.GetMetadataProvider(dataSourceName);
+ }
+ catch (Exception)
+ {
+ return McpResponseBuilder.BuildErrorResult("EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger);
+ }
+
+ if (!sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? dbObject) || dbObject is null)
+ {
+ return McpResponseBuilder.BuildErrorResult("EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger);
+ }
+
+ // Validate it's a table or view
+ if (dbObject.SourceType != EntitySourceType.Table && dbObject.SourceType != EntitySourceType.View)
+ {
+ return McpResponseBuilder.BuildErrorResult("InvalidEntity", $"Entity '{entityName}' is not a table or view. Use 'execute-entity' for stored procedures.", logger);
+ }
+
+ // 5) Authorization
+ IAuthorizationResolver authResolver = serviceProvider.GetRequiredService();
+ IHttpContextAccessor httpContextAccessor = serviceProvider.GetRequiredService();
+ HttpContext? httpContext = httpContextAccessor.HttpContext;
+
+ if (!McpAuthorizationHelper.ValidateRoleContext(httpContext, authResolver, out string roleError))
+ {
+ return McpResponseBuilder.BuildErrorResult("PermissionDenied", $"Permission denied: {roleError}", logger);
+ }
+
+ if (!McpAuthorizationHelper.TryResolveAuthorizedRole(
+ httpContext!,
+ authResolver,
+ entityName,
+ EntityActionOperation.Delete,
+ out string? effectiveRole,
+ out string authError))
+ {
+ return McpResponseBuilder.BuildErrorResult("PermissionDenied", $"Permission denied: {authError}", logger);
+ }
+
+ // 6) Build and validate Delete context
+ RequestValidator requestValidator = new(metadataProviderFactory, runtimeConfigProvider);
+
+ DeleteRequestContext context = new(
+ entityName: entityName,
+ dbo: dbObject,
+ isList: false);
+
+ foreach (KeyValuePair kvp in keys)
+ {
+ if (kvp.Value is null)
+ {
+ return McpResponseBuilder.BuildErrorResult("InvalidArguments", $"Primary key value for '{kvp.Key}' cannot be null.", logger);
+ }
+
+ context.PrimaryKeyValuePairs[kvp.Key] = kvp.Value;
+ }
+
+ requestValidator.ValidatePrimaryKey(context);
+
+ // 7) Execute
+ DatabaseType dbType = config.GetDataSourceFromDataSourceName(dataSourceName).DatabaseType;
+ IMutationEngine mutationEngine = mutationEngineFactory.GetMutationEngine(dbType);
+
+ IActionResult? mutationResult = null;
+ try
+ {
+ // Cancellation check before executing
+ cancellationToken.ThrowIfCancellationRequested();
+ mutationResult = await mutationEngine.ExecuteAsync(context).ConfigureAwait(false);
+ }
+ catch (DataApiBuilderException dabEx)
+ {
+ // Handle specific DAB exceptions
+ logger?.LogError(dabEx, "Data API Builder error deleting record from {Entity}", entityName);
+
+ string message = dabEx.Message;
+
+ // Check for specific error patterns
+ if (message.Contains("Could not find item with", StringComparison.OrdinalIgnoreCase))
+ {
+ string keyDetails = McpJsonHelper.FormatKeyDetails(keys);
+ return McpResponseBuilder.BuildErrorResult(
+ "RecordNotFound",
+ $"No record found with the specified primary key: {keyDetails}",
+ logger);
+ }
+ else if (message.Contains("violates foreign key constraint", StringComparison.OrdinalIgnoreCase) ||
+ message.Contains("REFERENCE constraint", StringComparison.OrdinalIgnoreCase))
+ {
+ return McpResponseBuilder.BuildErrorResult(
+ "ConstraintViolation",
+ "Cannot delete record due to foreign key constraint. Other records depend on this record.",
+ logger);
+ }
+ else if (message.Contains("permission", StringComparison.OrdinalIgnoreCase) ||
+ message.Contains("authorization", StringComparison.OrdinalIgnoreCase))
+ {
+ return McpResponseBuilder.BuildErrorResult(
+ "PermissionDenied",
+ "You do not have permission to delete this record.",
+ logger);
+ }
+ else if (message.Contains("invalid", StringComparison.OrdinalIgnoreCase) &&
+ message.Contains("type", StringComparison.OrdinalIgnoreCase))
+ {
+ return McpResponseBuilder.BuildErrorResult(
+ "InvalidArguments",
+ "Invalid data type for one or more key values.",
+ logger);
+ }
+
+ // For any other DAB exceptions, return the message as-is
+ return McpResponseBuilder.BuildErrorResult(
+ "DataApiBuilderError",
+ dabEx.Message,
+ logger);
+ }
+ catch (SqlException sqlEx)
+ {
+ // Handle SQL Server specific errors
+ logger?.LogError(sqlEx, "SQL Server error deleting record from {Entity}", entityName);
+ string errorMessage = sqlEx.Number switch
+ {
+ 547 => "Cannot delete record due to foreign key constraint. Other records depend on this record.",
+ 2627 or 2601 => "Cannot delete record due to unique constraint violation.",
+ 229 or 262 => $"Permission denied to delete from table '{dbObject.FullName}'.",
+ 208 => $"Table '{dbObject.FullName}' not found in the database.",
+ _ => $"Database error: {sqlEx.Message}"
+ };
+ return McpResponseBuilder.BuildErrorResult("DatabaseError", errorMessage, logger);
+ }
+ catch (DbException dbEx)
+ {
+ // Handle generic database exceptions (works for PostgreSQL, MySQL, etc.)
+ logger?.LogError(dbEx, "Database error deleting record from {Entity}", entityName);
+
+ // Check for common patterns in error messages
+ string errorMsg = dbEx.Message.ToLowerInvariant();
+ if (errorMsg.Contains("foreign key") || errorMsg.Contains("constraint"))
+ {
+ return McpResponseBuilder.BuildErrorResult(
+ "ConstraintViolation",
+ "Cannot delete record due to foreign key constraint. Other records depend on this record.",
+ logger);
+ }
+ else if (errorMsg.Contains("not found") || errorMsg.Contains("does not exist"))
+ {
+ return McpResponseBuilder.BuildErrorResult(
+ "RecordNotFound",
+ "No record found with the specified primary key.",
+ logger);
+ }
+
+ return McpResponseBuilder.BuildErrorResult("DatabaseError", $"Database error: {dbEx.Message}", logger);
+ }
+ catch (InvalidOperationException ioEx) when (ioEx.Message.Contains("connection", StringComparison.OrdinalIgnoreCase))
+ {
+ // Handle connection-related issues
+ logger?.LogError(ioEx, "Database connection error");
+ return McpResponseBuilder.BuildErrorResult("ConnectionError", "Failed to connect to the database.", logger);
+ }
+ catch (TimeoutException timeoutEx)
+ {
+ // Handle query timeout
+ logger?.LogError(timeoutEx, "Delete operation timeout for {Entity}", entityName);
+ return McpResponseBuilder.BuildErrorResult("TimeoutError", "The delete operation timed out.", logger);
+ }
+ catch (Exception ex)
+ {
+ string errorMsg = ex.Message ?? string.Empty;
+
+ if (errorMsg.Contains("Could not find", StringComparison.OrdinalIgnoreCase) ||
+ errorMsg.Contains("record not found", StringComparison.OrdinalIgnoreCase))
+ {
+ string keyDetails = McpJsonHelper.FormatKeyDetails(keys);
+ return McpResponseBuilder.BuildErrorResult(
+ "RecordNotFound",
+ $"No entity found with the given key {keyDetails}.",
+ logger);
+ }
+ else
+ {
+ // Re-throw unexpected exceptions
+ throw;
+ }
+ }
+
+ // 8) Build response
+ // Based on SqlMutationEngine, delete operations typically return NoContentResult
+ // We build a success response with just the operation details
+ Dictionary responseData = new()
+ {
+ ["entity"] = entityName,
+ ["keyDetails"] = McpJsonHelper.FormatKeyDetails(keys),
+ ["message"] = "Record deleted successfully"
+ };
+
+ // If the mutation result is OkObjectResult (which would be unusual for delete),
+ // include the result value directly without re-serialization
+ if (mutationResult is OkObjectResult okObjectResult && okObjectResult.Value is not null)
+ {
+ responseData["result"] = okObjectResult.Value;
+ }
+
+ return McpResponseBuilder.BuildSuccessResult(
+ responseData,
+ logger,
+ $"DeleteRecordTool success for entity {entityName}."
+ );
+ }
+ catch (OperationCanceledException)
+ {
+ return McpResponseBuilder.BuildErrorResult("OperationCanceled", "The delete operation was canceled.", logger);
+ }
+ catch (ArgumentException argEx)
+ {
+ return McpResponseBuilder.BuildErrorResult("InvalidArguments", argEx.Message, logger);
+ }
+ catch (Exception ex)
+ {
+ ILogger? innerLogger = serviceProvider.GetService>();
+ innerLogger?.LogError(ex, "Unexpected error in DeleteRecordTool.");
+
+ return McpResponseBuilder.BuildErrorResult(
+ "UnexpectedError",
+ "An unexpected error occurred during the delete operation.",
+ logger);
+ }
+ }
+ }
+}
diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs
new file mode 100644
index 0000000000..95c53d1d28
--- /dev/null
+++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs
@@ -0,0 +1,387 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json;
+using Azure.DataApiBuilder.Config.ObjectModel;
+using Azure.DataApiBuilder.Core.Configurations;
+using Azure.DataApiBuilder.Mcp.Model;
+using Azure.DataApiBuilder.Mcp.Utils;
+using Azure.DataApiBuilder.Service.Exceptions;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using ModelContextProtocol.Protocol;
+using static Azure.DataApiBuilder.Mcp.Model.McpEnums;
+
+namespace Azure.DataApiBuilder.Mcp.BuiltInTools
+{
+ ///
+ /// Tool to describe all entities configured in DAB, including their types and metadata.
+ ///
+ public class DescribeEntitiesTool : IMcpTool
+ {
+ ///
+ /// Gets the type of the tool, which is BuiltIn for this implementation.
+ ///
+ public ToolType ToolType { get; } = ToolType.BuiltIn;
+
+ ///
+ /// Gets the metadata for the delete-record tool, including its name, description, and input schema.
+ ///
+ ///
+ public Tool GetToolMetadata()
+ {
+ return new Tool
+ {
+ Name = "describe_entities",
+ Description = "Lists and describes all entities in the database, including their types and available operations.",
+ InputSchema = JsonSerializer.Deserialize(
+ @"{
+ ""type"": ""object"",
+ ""properties"": {
+ ""nameOnly"": {
+ ""type"": ""boolean"",
+ ""description"": ""If true, only entity names and descriptions will be returned. If false, full metadata including fields, parameters etc. will be included. Default is false.""
+ },
+ ""entities"": {
+ ""type"": ""array"",
+ ""items"": {
+ ""type"": ""string""
+ },
+ ""description"": ""Optional list of specific entity names to filter by. If empty, all entities will be described.""
+ }
+ }
+ }"
+ )
+ };
+ }
+
+ ///
+ /// Executes the DescribeEntities tool, returning metadata about configured entities.
+ ///
+ public Task ExecuteAsync(
+ JsonDocument? arguments,
+ IServiceProvider serviceProvider,
+ CancellationToken cancellationToken = default)
+ {
+ ILogger? logger = serviceProvider.GetService>();
+
+ try
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ RuntimeConfigProvider runtimeConfigProvider = serviceProvider.GetRequiredService();
+ RuntimeConfig runtimeConfig = runtimeConfigProvider.GetConfig();
+
+ if (!IsToolEnabled(runtimeConfig))
+ {
+ return Task.FromResult(McpResponseBuilder.BuildErrorResult(
+ "ToolDisabled",
+ $"The {GetToolMetadata().Name} tool is disabled in the configuration.",
+ logger));
+ }
+
+ (bool nameOnly, HashSet? entityFilter) = ParseArguments(arguments, logger);
+
+ List> entityList = new();
+
+ if (runtimeConfig.Entities != null)
+ {
+ foreach (KeyValuePair entityEntry in runtimeConfig.Entities)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ string entityName = entityEntry.Key;
+ Entity entity = entityEntry.Value;
+
+ if (!ShouldIncludeEntity(entityName, entityFilter))
+ {
+ continue;
+ }
+
+ try
+ {
+ Dictionary entityInfo = nameOnly
+ ? BuildBasicEntityInfo(entityName, entity)
+ : BuildFullEntityInfo(entityName, entity);
+
+ entityList.Add(entityInfo);
+ }
+ catch (Exception ex)
+ {
+ logger?.LogWarning(ex, "Failed to build info for entity {EntityName}", entityName);
+ }
+ }
+ }
+
+ if (entityList.Count == 0)
+ {
+ if (entityFilter != null && entityFilter.Count > 0)
+ {
+ return Task.FromResult(McpResponseBuilder.BuildErrorResult(
+ "EntitiesNotFound",
+ $"No entities found matching the filter: {string.Join(", ", entityFilter)}",
+ logger));
+ }
+ else
+ {
+ return Task.FromResult(McpResponseBuilder.BuildErrorResult(
+ "NoEntitiesConfigured",
+ "No entities are configured in the runtime configuration.",
+ logger));
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ entityList = entityList.OrderBy(e => e["name"]?.ToString() ?? string.Empty).ToList();
+
+ List