diff --git a/CHANGELOG.md b/CHANGELOG.md index a97c15e..84208c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ - `throughput` write subcommands (`set`/`manual`/`autoscale`) now accept `--dry-run` to preview the change — reporting current vs. planned mode and RU/s as JSON (and a table interactively) — without applying it or prompting for confirmation. A first slice of dry-run mode (item G1). ([#164](https://github.com/Azure/CosmosDBShell/issues/164)) - **`--dry-run` for destructive delete commands.** `rm`, `rmcon`, `rmdb`, and `delete` accept `--dry-run` to preview the effect without deleting anything: `rm` reports how many items match the pattern, and `rmcon`/`rmdb` report the container or database that would be removed. No confirmation prompt is shown and no changes are made. ([#156](https://github.com/Azure/CosmosDBShell/issues/156)) +### Improvements + +- **Structured (JSON) tool results for MCP.** MCP tool results now carry the machine-readable JSON payload (`result`/`outputText`/`error` plus `currentLocation`) as first-class `structuredContent` in addition to the existing JSON text block, so agents can consume structured results directly. The two representations are kept byte-for-byte equivalent, and text-only clients are unaffected. ([#154](https://github.com/Azure/CosmosDBShell/issues/154)) + ## 1.1.115-preview — 2026-07-01 A large feature cycle on top of 1.1.4-preview. The shell gains a stack of new commands — configurable **color themes**, a native **`filter`** language, change-feed **`watch`**, **`index`** and **`throughput`** management, bulk **`import`/`export`**, and server-side **`sproc`/`udf`/`trigger`** programming — plus a reworked **`info`** command (formerly `settings`) with usage statistics and JSON output, first-class observability through **`--diagnostics`** and **`--otel`**, and hardening of the MCP server. diff --git a/CosmosDBShell.Tests/McpResponseFactoryTests.cs b/CosmosDBShell.Tests/McpResponseFactoryTests.cs index 99f92a2..4a9088b 100644 --- a/CosmosDBShell.Tests/McpResponseFactoryTests.cs +++ b/CosmosDBShell.Tests/McpResponseFactoryTests.cs @@ -103,4 +103,45 @@ public void CreateSuccess_WhenDisconnected_UsesNullCurrentLocation() Assert.False(result.IsError); Assert.Equal(JsonValueKind.Null, document.RootElement.GetProperty("currentLocation").ValueKind); } + + [Fact] + public void CreateSuccess_PopulatesStructuredContentMatchingTextBlock() + { + var commandState = new CommandState + { + Result = new ShellJson(JsonSerializer.SerializeToElement(new + { + connected = true, + })), + }; + + var result = McpResponseFactory.CreateSuccess(commandState, new ContainerState("TestContainer", "TestDatabase", null!)); + var text = Assert.IsType(Assert.Single(result.Content)).Text; + + Assert.NotNull(result.StructuredContent); + var structured = result.StructuredContent!.Value; + + // The structured payload and the text rendering must stay in lockstep so clients + // that read either representation observe the same contract. + Assert.Equal(text, structured.GetRawText()); + Assert.True(structured.GetProperty("result").GetProperty("connected").GetBoolean()); + Assert.Equal("/TestDatabase/TestContainer", structured.GetProperty("currentLocation").GetString()); + } + + [Fact] + public void CreateError_PopulatesStructuredContentMatchingTextBlock() + { + var result = McpResponseFactory.CreateError("boom", new DatabaseState("TestDatabase", null!)); + var text = Assert.IsType(Assert.Single(result.Content)).Text; + + Assert.True(result.IsError); + Assert.NotNull(result.StructuredContent); + var structured = result.StructuredContent!.Value; + + // The structured payload and the text rendering must stay in lockstep so clients + // that read either representation observe the same contract. + Assert.Equal(text, structured.GetRawText()); + Assert.Equal("boom", structured.GetProperty("error").GetString()); + Assert.Equal("/TestDatabase", structured.GetProperty("currentLocation").GetString()); + } } \ No newline at end of file diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Mcp/McpResponseFactory.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Mcp/McpResponseFactory.cs index 698cdc7..c7c4249 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Mcp/McpResponseFactory.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Mcp/McpResponseFactory.cs @@ -57,13 +57,22 @@ private static CallToolResult CreateResponse(JsonObject payload, State shellStat { payload["currentLocation"] = GetCurrentLocation(shellState); + // Serialize the payload exactly once. The text content block reuses the structured + // element's raw JSON so the two representations are inherently byte-for-byte identical + // and no second serializer can drift out of sync. + var structuredContent = JsonSerializer.SerializeToElement(payload, JsonOptions); + return new CallToolResult { + // Emit the machine-readable payload as first-class structured content while + // retaining an equivalent text rendering so that clients that only read text + // content blocks continue to work. + StructuredContent = structuredContent, Content = [ new TextContentBlock { - Text = payload.ToJsonString(JsonOptions), + Text = structuredContent.GetRawText(), } ], IsError = isError, diff --git a/docs/mcp.md b/docs/mcp.md index d29fc40..d8f4d00 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -70,3 +70,24 @@ Your MCP client may use a remote LLM. Command outputs, query results, and file c - [ ] Review and approve destructive operations manually - [ ] Don't share secrets (keys, PII) in prompts or outputs - [ ] Disable MCP mode when not actively using it + +## Tool Results + +Every tool result carries the same machine-readable JSON payload in two places: + +- **`structuredContent`** — the payload as first-class structured content, for clients that consume MCP structured results. +- **A text content block** — the identical payload serialized as JSON text, so clients that only read text content blocks continue to work. + +Both representations are always byte-for-byte equivalent. + +### Payload shape + +| Field | When present | Description | +| ----- | ------------ | ----------- | +| `result` | Successful commands that produce output | The command result as JSON (objects, arrays, or a scalar). Text-only results are represented as a JSON string. | +| `outputText` | CSV output commands with non-empty text | The CSV rendering of the result. Omitted when the CSV output is empty or whitespace. | +| `error` | Failed commands | The error message. | +| `currentLocation` | Always | The shell's current navigation path (for example `/MyDatabase/MyContainer`), or `null` when disconnected. | + +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. +