Azure / Azure/data-api-builder
[Bug]: --mcp-stdio never infers database objects for configured entities in 2.1.x (works in 2.0.12, works over REST)
- Ngôn ngữ chính
- C#
- Star
- 1.5k
- Fork
- 370
- Merge trung bình
- 3 ngày 22 giờ
- Pull request đã merge (30 ngày)
- 9
Mô tả
## Describe the bug
In every `2.1.x` build, `dab start --mcp-stdio` **registers entities but never infers their database objects**. The MCP handshake succeeds, `tools/list` returns the full tool surface, and then every tool call fails:
```
Database object for entity 'MyTable' has not been inferred.
```
`read_records` reports the same condition differently:
```
Entity 'MyTable' is not defined in the configuration.
```
The same config, on the same machine, in the same minute, works perfectly over REST.
This does not reproduce on `2.0.12`.
## Repro
`dab-config.json` — one entity, nothing exotic:
```json
{
"data-source": {
"database-type": "mssql",
"connection-string": "@env('MY_CONN')"
},
"runtime": {
"rest": { "enabled": true, "path": "/api" },
"graphql": { "enabled": false },
"host": {
"mode": "development",
"cors": { "origins": [], "allow-credentials": false },
"authentication": { "provider": "StaticWebApps" }
},
"mcp": {
"enabled": true,
"dml-tools": {
"describe-entities": true,
"create-record": false,
"update-record": false,
"delete-record": false,
"execute-entity": true
}
}
},
"entities": {
"MyTable": {
"source": { "object": "dbo.MyTable", "type": "table" },
"permissions": [{ "role": "anonymous", "actions": ["read"] }]
}
}
}
```
(`$schema` is omitted deliberately — the repro behaves the same with no `$schema`, with the
`v1.7.93` URL, and with a `v2.1.3-rc` URL, so it is not a factor.)
**Fails** — drive the stdio server by hand, so no MCP client is involved:
```bash
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"describe_entities","arguments":{}}}' \
| dab start --mcp-stdio
```
```json
{"toolName":"describe_entities","status":"error",
"error":{"type":"DataApiBuilderError",
"message":"Database object for entity 'MyTable' has not been inferred."}}
```
**Works** — identical config, identical connection string, same shell:
```bash
dab start # REST mode
curl "http://localhost:5000/api/MyTable?\$first=1"
```
`HTTP 200`, real rows, all columns.
## Expected behaviour
`--mcp-stdio` should infer database objects for configured entities, exactly as REST mode does.
## Version bracket
| version | published | `--mcp-stdio` | REST |
|---|---|---|---|
| **2.0.12** (GA) | 2026-08-20 | **PASS** | PASS |
| 2.1.0-rc | 2026-08-11 | **FAIL** | PASS |
| 2.1.3-rc | 2026-08-25 | **FAIL** | PASS |
Same machine, same config, same database, same connection string throughout.
## What we ruled out
- **Config shape** — the minimal one-entity config above fails identically to our real 28-entity config.
- **Config discovery** — an explicit `--config` with an absolute path changes nothing. Running from a directory containing *no* config produces no response at all, which is a different failure, so the file is definitely being found and parsed.
- **Timing / lazy init** — waiting 8 s between `initialize` and the tool call makes no difference. (We checked this because of #3430.)
- **The MCP client** — the repro above is raw JSON-RPC piped into stdin, no client involved.
- **Connectivity, credentials, permissions** — REST serves the same entity from the same connection string; an independent SQL client sees the table and rows as the same login.
## Root cause
Confirmed by reading `main`, not inferred from behaviour. Schema inference is reachable **only** through the ASP.NET Core startup path, which stdio mode skips:
**1. `src/Service/Program.cs` — `StartEngine` returns before the host is ever started**
```csharp
IHost host = CreateHostBuilder(args, runMcpStdio, mcpRole).Build();
if (runMcpStdio)
{
return McpStdioHelper.RunMcpStdioHost(host); // <-- returns here
}
// Normal web mode
host.Run();
```
**2. `src/Service/Utilities/McpStdioHelper.cs` — `RunMcpStdioHost` initialises tools, and only tools**
```csharp
Mcp.Core.McpToolRegistry.InitializeAndRegisterTools(tools, registry, host.Services);
```
No `host.StartAsync()`, so `Startup.Configure` never runs.
**3. `src/Service/Startup.cs:822` — `Configure` is where inference is triggered**
```csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ...)
...
// line 866
isRuntimeReady = PerformOnConfigChangeAsync(app).Result;
```
**4. `src/Service/Startup.cs:1431` — and that is the only caller of `InitializeAsync`**
```csharp
private async Task PerformOnConfigChangeAsync(IApplicationBuilder app)
{
...
IMetadataProviderFactory sqlMetadataProviderFactory =
app.ApplicationServices.GetRequiredService();
await sqlMetadataProviderFactory.InitializeAsync(); // <-- schema inference
```
So in stdio mode the tool registry knows the entity names (they come from config), while `IMetadataProviderFactory` is never initialised and no entity ever gets a database object. That is exactly the observed split: `tools/list` succeeds, every tool call fails.
This is a side effect of **#3676 "Avoid starting web host in MCP stdio mode"** (merged 2026-07-09, fixing #3675). That change was correct in itself — stdio mode should not bind an HTTP port — but `PerformOnConfigChangeAsync` did more than serve HTTP, and nothing took over its metadata-initialisation duty on the stdio path.
### Suggested fix
Have `RunMcpStdioHost` initialise the metadata provider before entering the JSON-RPC loop — either by calling `IMetadataProviderFactory.InitializeAsync()` directly from DI, or by extracting the non-HTTP portion of `PerformOnConfigChangeAsync` (config validation + metadata init) into something both paths call. `RunMcpStdioHost` already resolves services from `host.Services`, so the plumbing is there.
Worth noting for whoever picks this up: #3430 reports that stdio blocks the `initialize` response until introspection finishes. Restoring inference on this path will bring that latency back, so the two probably want solving together.
## Environment
- **DAB:** 2.1.0-rc and 2.1.3-rc fail; 2.0.12 works
- **Install:** `dotnet tool install -g microsoft.dataapibuilder --version `
- **.NET:** 8.0.30 runtime + ASP.NET Core 8.0.30 (for 2.0.x); 10.0.11 (for 2.1.x)
- **OS:** CachyOS Linux (Arch), x64, kernel 7.2.0
- **Database:** on-premises SQL Server over TCP on a non-default port (not Azure SQL)
- **Connection string:** `Encrypt=True;TrustServerCertificate=True`
- **MCP client:** none — raw JSON-RPC on stdin
## Side note
`--prerelease` currently resolves to `2.1.3-rc`, so anyone following the common `dotnet tool install -g microsoft.dataapibuilder --prerelease` advice lands on a build where MCP stdio cannot serve any entity. `--version 2.0.12` is the working install today.
---
*Investigated and written with Claude Code (**Νύξ**) 🌑 — version bracketing, the REST/stdio control and the root-cause trace were worked out together over one session. A fix is on the way as a PR.*
Hướng dẫn đóng góp
Hướng nghiên cứu
Bắt đầu trong src/Service/Program.cs và src/Service/Utilities/McpStdioHelper.cs, sau đó so sánh đường dẫn stdio với Startup.cs quanh các dòng 822, 866 và 1431. Chạy bản tái hiện JSON-RPC thô và xác minh rằng việc khởi tạo siêu dữ liệu xảy ra trước các lệnh gọi công cụ, trong khi hành vi REST vẫn không thay đổi và describe_entities có thể truy cập đối tượng cơ sở dữ liệu đã được cấu hình.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- csharp, sql
- Lĩnh vực
- api, backend, databases
- Loại issue
- Lỗi
- Độ khó
- 3/5
- Thời gian dự kiến
- 1-2 ngày
- Mức độ hoạt động
- Sôi nổi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức phù hợp với người mới
- 68/100