Skip to content

Surface Cosmos request charge (RUs) in MCP structured results - #171

Open
Mike Krüger (mkrueger) wants to merge 8 commits into
mainfrom
dev/mkrueger/request-charge-surfacing
Open

Surface Cosmos request charge (RUs) in MCP structured results#171
Mike Krüger (mkrueger) wants to merge 8 commits into
mainfrom
dev/mkrueger/request-charge-surfacing

Conversation

@mkrueger

@mkrueger Mike Krüger (mkrueger) commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Relates to #162 (implements part (a); does not close the issue).

Summary

Surfaces the Cosmos DB request charge (RUs) consumed by data-plane commands as a uniform, machine-readable field on MCP tool results. This addresses part (a) of #162 (RU-cost surfacing) -- consistent RU reporting in structured output. It does not include the budget/--max-ru guardrails (part b), which are proposed as a separate follow-up.

Today RU is reported ad-hoc and inconsistently: query includes it in structured output only in one code path, most write commands print it to the console as a formatted string, and reads/deletes don't report it at all. This change makes RU a first-class, consistent part of the MCP structured contract.

What changed

  • CommandState.RequestCharge -- a new nullable double that data-plane commands set with the RUs consumed.
  • MCP payload -- McpResponseFactory emits a top-level requestCharge number in the structured tool result (and the equivalent text block) whenever it is set. The result document shape is unchanged; requestCharge is a sibling metadata field, so existing result parsers and CSV/table rendering are unaffected.
  • Commands retrofitted to record the charge on the returned state:
    • query -- accumulates TotalRequestCharge across pages
    • print -- point-read charge from the response headers
    • mkitem, replace, patch -- single and bulk write charges
    • rm -- accumulates delete charges (previously untracked)
    • import / export -- bulk aggregate charge
    • sproc exec -- stored-procedure execution charge

Design notes

  • RU is surfaced as payload metadata rather than being injected into the result document, to avoid changing the document contract that clients parse.
  • Management operations (e.g. creating/replacing sproc/udf/trigger definitions) are intentionally out of scope; this focuses on data-plane reads/writes.

Docs

Testing

  • New McpResponseFactory tests: requestCharge present when set, omitted when unset.
  • Extended the sproc exec test to assert the captured charge.
  • Full offline suite green (1793 tests); main project builds with 0 warnings / 0 errors.

Stacking

Based on dev/mkrueger/mcp-structured-content (#168) because the requestCharge field extends the MCP structuredContent payload introduced there. GitHub will retarget the base to main once #168 merges.

Part of the Agentic & Automation roadmap, item G2.

Fixes #162

@github-code-quality

github-code-quality Bot commented Jul 8, 2026

Copy link
Copy Markdown

Code Coverage Overview

Languages: C#

C# / code-coverage/dotnet

The overall line coverage in commit 515ab16 in the dev/mkrueger/request... branch remains at 61%, unchanged from commit 2fe15b7 in the main branch.

Show a line coverage summary of the most impacted files.
File main 2fe15b7 dev/mkrueger/request... 515ab16 +/-
D:\a\CosmosDBSh...ds\RmCommand.cs 19% 17% -2%
D:\a\CosmosDBSh...QueryCommand.cs 41% 41% 0%
D:\a\CosmosDBSh...mportCommand.cs 46% 46% 0%
D:\a\CosmosDBSh...xportCommand.cs 36% 36% 0%
D:\a\CosmosDBSh...PatchCommand.cs 28% 28% 0%
D:\a\CosmosDBSh...ponseFactory.cs 92% 92% 0%
D:\a\CosmosDBSh...eItemCommand.cs 16% 16% 0%
D:\a\CosmosDBSh...PrintCommand.cs 10% 10% 0%
D:\a\CosmosDBSh...\ListCommand.cs 16% 17% +1%
D:\a\CosmosDBSh...SprocCommand.cs 51% 52% +1%

Updated August 28, 2026 14:48 UTC

@mkrueger
Mike Krüger (mkrueger) force-pushed the dev/mkrueger/mcp-structured-content branch 2 times, most recently from 298a962 to ae7fc14 Compare July 8, 2026 08:59
Add a uniform RequestCharge property to CommandState and emit it as a top-level 'requestCharge' field in the MCP tool result payload. Retrofit data-plane commands (query, print, mkitem, replace, patch, rm, import, export, and sproc exec) to record the request units consumed so agents can track RU cost consistently across calls. The document result shape is unchanged; requestCharge is a sibling metadata field.

Addresses part (a) of #162.
@mkrueger
Mike Krüger (mkrueger) force-pushed the dev/mkrueger/request-charge-surfacing branch from 3cdc2f2 to 044df91 Compare July 8, 2026 10:12
@mkrueger
Mike Krüger (mkrueger) changed the base branch from dev/mkrueger/mcp-structured-content to main July 8, 2026 11:17
@mkrueger
Mike Krüger (mkrueger) requested a review from a team July 8, 2026 11:17
…charge-surfacing

# Conflicts:
#	CHANGELOG.md
#	CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/RmCommand.cs
Copilot AI lite review requested due to automatic review settings July 13, 2026 09:11

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

This PR adds a consistent way for data-plane commands to report Cosmos DB request charge (RUs) by storing it on CommandState and emitting it as a top-level requestCharge field in MCP tool-result payloads (both structuredContent and the legacy JSON text block).

Changes:

  • Add CommandState.RequestCharge and include it as requestCharge in MCP success payloads when set.
  • Retrofit multiple data-plane commands (query, print, mkitem, replace, patch, rm, import, export, sproc exec) to capture RU charge.
  • Document the MCP payload shape and add/extend tests validating structuredContent parity and requestCharge behavior.

Reviewed changes

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

Show a summary per file
File Description
docs/mcp.md Documents tool-result payload shape and the new requestCharge metadata field.
CosmosDBShell/Azure.Data.Cosmos.Shell.Mcp/McpResponseFactory.cs Emits StructuredContent and adds requestCharge to the MCP success payload when available.
CosmosDBShell/Azure.Data.Cosmos.Shell.Core/CommandState.cs Introduces the nullable RequestCharge field for commands to populate.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/QueryCommand.cs Attempts to aggregate query RU charges and store them on CommandState.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/PrintCommand.cs Captures point-read RU charge from response headers.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeItemCommand.cs Accumulates RU charges across single/bulk writes and propagates to returned state.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ReplaceCommand.cs Returns total replace charge (single/bulk) and sets it on the returned state.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/PatchCommand.cs Sets request charge from patch response.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/RmCommand.cs Tracks delete RU charges and sets them on the command state.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ImportCommand.cs Sets aggregated RU charge on success.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/ExportCommand.cs Sets aggregated RU charge on success.
CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/SprocCommand.cs Captures RU charge from stored procedure execution.
CosmosDBShell.Tests/McpResponseFactoryTests.cs Adds tests for StructuredContent population/parity and requestCharge presence/omission.
CosmosDBShell.Tests/CommandTests/SprocCommandExecutionTests.cs Extends sproc exec test to assert RequestCharge is captured.
CHANGELOG.md Adds entries describing structured MCP results and RU surfacing.

Comment thread CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/QueryCommand.cs Outdated
Comment thread CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/RmCommand.cs Outdated
Copilot AI review requested due to automatic review settings July 13, 2026 13:39

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

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

Resolve command result conflicts by preserving main's structured output envelopes together with uniform request-charge metadata.
Copilot AI review requested due to automatic review settings August 25, 2026 09:57

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

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Suppressed comments (5)

CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/SprocCommand.cs:449

  • SprocCommand is marked Restricted = true without Confirmable, and ToolOperations rejects the sproc tool before invoking ExecAsync. Therefore this assignment is never observed by McpResponseFactory, while the PR documentation claims sproc exec reports requestCharge in MCP results. Either make this command callable/confirmable over MCP or remove it from this MCP-specific contract and the associated documentation.
            commandState.RequestCharge = response.RequestCharge;

CosmosDBShell/Azure.Data.Cosmos.Shell.Mcp/McpResponseFactory.cs:102

  • Container-scoped ls is also an MCP-exposed data-plane read: ListContainerItemsAsync runs GetItemQueryStreamIterator and consumes query pages, but it never populates CommandState.RequestCharge. As a result, the new factory still omits requestCharge for ls, so the documented uniform data-plane contract is incomplete. Accumulate each page's request charge and assign it to the returned state (with a focused test).
        if (commandState.RequestCharge.HasValue)
        {
            payload["requestCharge"] = commandState.RequestCharge.Value;
        }

CosmosDBShell/Azure.Data.Cosmos.Shell.Mcp/McpResponseFactory.cs:102

  • Error responses bypass this block: ToolOperations catches command exceptions and calls CreateError, whose payload contains only error and currentLocation. A CosmosException can carry a non-zero RequestCharge even when a read or write fails, so those MCP results still omit the cost despite the contract describing requestCharge for data-plane operations. Propagate the charge through the error path, or explicitly document that the field is success-only.
        if (commandState.RequestCharge.HasValue)
        {
            payload["requestCharge"] = commandState.RequestCharge.Value;
        }

docs/mcp.md:105

  • The documented list is incomplete for the stated uniform data-plane contract: ls item listing, watch, and info --partitions/--detailed also issue item/change-feed queries, but their returned states never populate CommandState.RequestCharge (see ListCommand.cs:228-230, WatchCommand.cs:173-175, and InfoCommand.cs:490-550). Clients therefore still receive no RU field for these MCP calls. Instrument those paths as well, or explicitly narrow this contract to the commands listed here.
Successful results set `result` (and optionally `outputText`); failed results set `error` and mark the tool result as an error. `currentLocation` is always included so a client can track navigation state across calls. Data-plane commands (`query`, `print`, `mkitem`, `replace`, `patch`, `rm`, `import`, `export`, and `sproc exec`) additionally set `requestCharge` so a client can track RU cost across calls.

docs/mcp.md:105

  • sproc exec cannot be invoked through MCP: SprocCommand is marked restricted without a confirmable path, and ToolOperations rejects it before execution. This sentence therefore overstates the client-facing MCP contract by saying clients can track requestCharge for sproc exec; remove it from this MCP list or change the command's MCP availability consistently.
Successful results set `result` (and optionally `outputText`); failed results set `error` and mark the tool result as an error. `currentLocation` is always included so a client can track navigation state across calls. Data-plane commands (`query`, `print`, `mkitem`, `replace`, `patch`, `rm`, `import`, `export`, and `sproc exec`) additionally set `requestCharge` so a client can track RU cost across calls.

Comment thread CHANGELOG.md
Comment thread CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/RmCommand.cs Outdated
Comment thread docs/mcp.md Outdated
Copilot AI review requested due to automatic review settings August 28, 2026 14:08

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

Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.

Comment thread docs/mcp.md Outdated
Comment thread CHANGELOG.md
Comment thread CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/QueryCommand.cs Outdated
Comment thread CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/RmCommand.cs Outdated
Copilot AI review requested due to automatic review settings August 28, 2026 14:16

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

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

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

CosmosDBShell/Azure.Data.Cosmos.Shell.Mcp/McpResponseFactory.cs:108

  • requestCharge is only emitted for non-error command states because the commandState.IsError early-return happens before the RequestCharge block. If a command captures RU cost but returns an error CommandState (for example, partial failures that still have a request charge), the MCP payload will omit requestCharge, undermining the goal of uniform RU reporting.

        if (commandState.RequestCharge.HasValue)
        {
            payload["requestCharge"] = commandState.RequestCharge.Value;
        }

Copilot AI review requested due to automatic review settings August 28, 2026 14:29

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

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

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/RmCommand.cs:129

  • In --dry-run mode this helper returns (true, 0) even though no delete is issued, so the tuple field name Deleted is misleading (it actually means “counted as deleted / would be deleted”). This makes later uses like if (deleteResult.Deleted) read as if an item was actually deleted when it wasn’t.

Consider renaming the tuple element to something like WouldDelete/Counted (and updating the 3 call sites) to keep the meaning clear.

        // In dry-run mode, count what would be deleted without issuing any delete.
        async Task<(bool Deleted, double RequestCharge)> TryDeleteAsync(string id, PartitionKey partitionKey)
        {
            if (dryRun)
            {
                return (true, 0);

Copilot AI review requested due to automatic review settings August 28, 2026 14:39

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

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

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.

G2. RU-cost surfacing & budget guardrails

2 participants