Skip to content

[MCP] Initialize metadata providers in stdio mode - #3784

Open
Christos Despotakis (despotak) wants to merge 1 commit into
Azure:mainfrom
despotak:fix/mcp-stdio-metadata-inference
Open

[MCP] Initialize metadata providers in stdio mode#3784
Christos Despotakis (despotak) wants to merge 1 commit into
Azure:mainfrom
despotak:fix/mcp-stdio-metadata-inference

Conversation

@despotak

Copy link
Copy Markdown

Why make this change?

Closes #3783.

Summary of the linked issue: in every 2.1.x build, dab start --mcp-stdio registers entities but never infers their database objects, so every MCP tool call fails with Database object for entity '<name>' has not been inferred. while the identical config serves the same entity correctly over REST. 2.0.12 is unaffected.

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

Related: #3676, #3675 (the change this regressed from), and #3430 — see the note at the bottom.

What is this change?

Schema inference is reachable only through the ASP.NET Core startup path, which stdio mode skips:

  1. Program.csStartEngine returns before the host is started:
    IHost host = CreateHostBuilder(args, runMcpStdio, mcpRole).Build();
    
    if (runMcpStdio)
    {
        return McpStdioHelper.RunMcpStdioHost(host);   // returns here
    }
    
    host.Run();   // normal web mode
  2. McpStdioHelper.RunMcpStdioHost — initialises the tool registry and nothing else, so Startup.Configure never runs.
  3. Startup.cs:822Configure is the only place that calls PerformOnConfigChangeAsync(app) (line 866).
  4. Startup.cs:1431PerformOnConfigChangeAsync is the only caller of IMetadataProviderFactory.InitializeAsync().

So entity names reach the tool registry from config, while IMetadataProviderFactory is never initialised and no entity receives 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". That change was right — 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.

The fix is 10 lines: RunMcpStdioHost resolves IMetadataProviderFactory from DI and initialises it before registering tools. Program.cs is untouched, and #3676's behaviour is preserved — the existing assertions that StartAsync/StopAsync are never called still pass.

How was this tested?

  • Integration Tests
  • Unit Tests

RunMcpStdioHost_DoesNotStartWebHost gains a stub IMetadataProviderFactory and an assertion that InitializeAsync is called exactly once. Its original assertions are unchanged and still pass, so this cannot silently re-introduce the web host.

Also verified by hand against SQL Server, on main built from source with the pinned SDK (10.0.302), before and after the patch:

before after
describe_entities, 1-entity config has not been inferred success (1 entity)
describe_entities, 28-entity config has not been inferred success (28 entities)
read_records on a table error success, rows returned
REST GET /api/<entity> HTTP 200 HTTP 200 (unchanged)

dotnet format --verify-no-changes on both touched files exits 0.

Sample Request(s)

Driving 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

Before:

{"toolName":"describe_entities","status":"error",
 "error":{"type":"DataApiBuilderError",
          "message":"Database object for entity 'MyTable' has not been inferred."}}

After:

{"entities":[{"name":"MyTable","description":"","fields":[],"permissions":["READ"]}],
 "count":1,"status":"success"}

One note for reviewers

#3430 reports that stdio blocks the initialize response until introspection finishes (~17 s against a remote instance with 53 entities). Restoring inference on this path necessarily brings that latency back — it was only absent because inference was not happening at all. If you would prefer inference to run asynchronously after initialize returns, that is a larger change and I am happy to rework this accordingly; this PR deliberately restores correctness first.


Investigated and written with Claude Code (Νύξ) 🌑 — the version bracket, the REST/stdio control and the root-cause trace were worked out together. Reviewed and submitted by me.

dab start --mcp-stdio returns from Program.StartEngine before host.Run(), so
Startup.Configure never executes -- and with it PerformOnConfigChangeAsync, the
only caller of IMetadataProviderFactory.InitializeAsync(). Entity names reach the
tool registry from config, but no entity ever receives a database object, so every
MCP tool call fails with:

    Database object for entity '<name>' has not been inferred.

The identical configuration serves the same entity correctly over REST, because
the web path does call host.Run().

This is a side effect of Azure#3676 (Avoid starting web host in MCP stdio mode). 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-initialization duty on the stdio path.

RunMcpStdioHost now initializes the metadata providers itself, before registering
tools. The existing assertions that StartAsync and StopAsync are never called
still hold, so Azure#3676 is preserved; the unit test gains a stub factory and an
assertion that InitializeAsync is invoked exactly once.

Verified against SQL Server: describe_entities and read_records both succeed on a
one-entity and a twenty-eight-entity configuration, and REST is unchanged.

Fixes Azure#3783

Co-authored-by: Νύξ (Nyx) 🌑 <noreply@anthropic.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@despotak

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Restores schema inference for MCP stdio mode by explicitly initializing the metadata providers on the stdio startup path (which bypasses ASP.NET Core Startup.Configure). This addresses the 2.1.x regression where tools were registered from config but all tool calls failed because database objects were never inferred.

Changes:

  • Initialize IMetadataProviderFactory inside McpStdioHelper.RunMcpStdioHost before registering MCP tools.
  • Extend the existing unit test to assert IMetadataProviderFactory.InitializeAsync() is invoked exactly once in stdio mode.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/Service/Utilities/McpStdioHelper.cs Initializes metadata providers during stdio startup so entities have inferred database objects before tool calls.
src/Service.Tests/UnitTests/McpStdioHelperTests.cs Adds a stub IMetadataProviderFactory and asserts InitializeAsync() is called once without starting the web host.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants