Skip to content

[Bug]: --mcp-stdio never infers database objects for configured entities in 2.1.x (works in 2.0.12, works over REST) #3783

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:

{
  "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:

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
{"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:

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 --mcp-stdio blocks initialize response until all schema introspection completes, causing MCP reconnect timeouts #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.csStartEngine returns before the host is ever started

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.csRunMcpStdioHost initialises tools, and only tools

Mcp.Core.McpToolRegistry.InitializeAndRegisterTools(tools, registry, host.Services);

No host.StartAsync(), so Startup.Configure never runs.

3. src/Service/Startup.cs:822Configure is where inference is triggered

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

private async Task<bool> PerformOnConfigChangeAsync(IApplicationBuilder app)
{
    ...
    IMetadataProviderFactory sqlMetadataProviderFactory =
        app.ApplicationServices.GetRequiredService<IMetadataProviderFactory>();
    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 <v>
  • .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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions