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)
- Dominant language
- C#
- Stars
- 1.5k
- Forks
- 370
- Avg merge
- 3d 17h
- Merged PRs (30d)
- 8
Description
## 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.*
Contributor guide
Assessment
This issue has not been assessed yet.