Skip to content

Commit b04e8fe

Browse files
RobiladKCopilot
andcommitted
feat(monitor): add Basic and Auxiliary Log Analytics search
Add bounded workspace searches with typed results, table-plan validation, and support across MCP discovery modes. Include tests and documentation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 2e14a68 commit b04e8fe

40 files changed

Lines changed: 3972 additions & 20 deletions

core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/Discovery/CommandGroupDiscoveryStrategyTests.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.Extensions.Logging;
55
using Microsoft.Mcp.Core.Areas.Server;
66
using Microsoft.Mcp.Core.Areas.Server.Commands.Discovery;
7+
using Microsoft.Mcp.Core.Areas.Server.Options;
78
using Microsoft.Mcp.Tests.Client.Helpers;
89
using NSubstitute;
910
using Xunit;
@@ -153,6 +154,23 @@ public async Task DiscoverServersAsync_WithReadOnlyTrue_CreatesReadOnlyProviders
153154
Assert.All(result, provider => Assert.True(((CommandGroupServerProvider)provider).ReadOnly));
154155
}
155156

157+
[Theory]
158+
[InlineData(StructuredOutputMode.Duplicated)]
159+
[InlineData(StructuredOutputMode.Compact)]
160+
public async Task DiscoverServersAsync_WithStructuredOutputMode_ForwardsMode(
161+
StructuredOutputMode mode)
162+
{
163+
var configuration = new ServerRuntimeConfiguration { StructuredOutputMode = mode };
164+
var strategy = CreateStrategy(configuration: configuration);
165+
166+
var result = await strategy.DiscoverServersAsync(TestContext.Current.CancellationToken);
167+
168+
Assert.NotEmpty(result);
169+
Assert.All(
170+
result,
171+
provider => Assert.Equal(mode, ((CommandGroupServerProvider)provider).StructuredOutputMode));
172+
}
173+
156174
[Fact]
157175
public async Task DiscoverServersAsync_WithCustomEntryPoint_SetsEntryPointOnAllProviders()
158176
{

core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/Discovery/CommandGroupServerProviderTests.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using Microsoft.Mcp.Core.Areas.Server.Commands.Discovery;
5+
using Microsoft.Mcp.Core.Areas.Server.Options;
56
using Microsoft.Mcp.Core.Commands;
67
using Microsoft.Mcp.Tests.Client.Helpers;
78
using ModelContextProtocol.Client;
@@ -181,4 +182,42 @@ public void BuildArguments_WithCustomTransport_IncludesTransportFlag()
181182
var expected = new[] { "server", "start", "--mode", "all", "--namespace", "testGroup", "--transport", "custom-transport" };
182183
Assert.Equal(expected, arguments);
183184
}
185+
186+
[Theory]
187+
[InlineData(StructuredOutputMode.Duplicated, "duplicated")]
188+
[InlineData(StructuredOutputMode.Compact, "compact")]
189+
public void BuildArguments_WithStructuredOutputMode_ForwardsMode(
190+
StructuredOutputMode mode,
191+
string expectedMode)
192+
{
193+
var provider = new CommandGroupServerProvider(new CommandGroup("testGroup", "Test Description"))
194+
{
195+
StructuredOutputMode = mode
196+
};
197+
198+
var arguments = provider.BuildArguments();
199+
200+
Assert.Equal(
201+
[
202+
"server",
203+
"start",
204+
"--mode",
205+
"all",
206+
"--namespace",
207+
"testGroup",
208+
"--transport",
209+
"stdio",
210+
"--structured-output-mode",
211+
expectedMode
212+
],
213+
arguments);
214+
}
215+
216+
[Fact]
217+
public void BuildArguments_WithoutStructuredOutputMode_OmitsFlag()
218+
{
219+
var provider = new CommandGroupServerProvider(new CommandGroup("testGroup", "Test Description"));
220+
221+
Assert.DoesNotContain("--structured-output-mode", provider.BuildArguments());
222+
}
184223
}

core/Microsoft.Mcp.Core/src/Areas/Server/Commands/Discovery/CommandGroupDiscoveryStrategy.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public override Task<IEnumerable<IMcpServerProvider>> DiscoverServersAsync(Cance
4040
{
4141
ReadOnly = _configuration.Value.ReadOnly,
4242
Transport = _configuration.Value.Transport,
43+
StructuredOutputMode = _configuration.Value.StructuredOutputMode,
4344
EntryPoint = EntryPoint,
4445
})
4546
.Cast<IMcpServerProvider>();

core/Microsoft.Mcp.Core/src/Areas/Server/Commands/Discovery/CommandGroupServerProvider.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ public string? EntryPoint
3636
/// </summary>
3737
public string Transport { get; set; } = TransportTypes.StdIo;
3838

39+
/// <summary>
40+
/// Gets or sets the structured output mode forwarded to the child server.
41+
/// </summary>
42+
public StructuredOutputMode? StructuredOutputMode { get; set; }
43+
3944
/// <inheritdoc/>
4045
public async Task<McpClient> CreateClientAsync(McpClientOptions clientOptions, CancellationToken cancellationToken)
4146
{
@@ -70,6 +75,12 @@ internal string[] BuildArguments()
7075
arguments.Add($"--read-only");
7176
}
7277

78+
if (StructuredOutputMode.HasValue)
79+
{
80+
arguments.Add("--structured-output-mode");
81+
arguments.Add(StructuredOutputMode.Value.ToString().ToLowerInvariant());
82+
}
83+
7384
return [.. arguments];
7485
}
7586

core/Microsoft.Mcp.Core/src/Areas/Server/Commands/Discovery/ConsolidatedToolDiscoveryStrategy.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,10 @@ public void ConfigureServices(IServiceCollection services)
245245
public CommandGroup RegisterCommands(IServiceProvider serviceProvider)
246246
{
247247
// Create command group for this consolidated tool
248-
var commandGroup = new CommandGroup(Name, Title);
248+
var commandGroup = new CommandGroup(
249+
Name,
250+
_consolidatedTool.Description ?? Name,
251+
Title);
249252

250253
// Add all matching commands to this group
251254
foreach (var cmd in _matchingCommands)

core/Microsoft.Mcp.Core/src/Areas/Server/Commands/ToolLoading/NamespaceToolLoader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ public override ValueTask<ListToolsResult> ListToolsHandler(RequestContext<ListT
134134
var tool = new Tool
135135
{
136136
Name = namespaceName,
137-
Description = group.Description + """
137+
Description = group.Description + Environment.NewLine + Environment.NewLine + """
138138
This tool is a hierarchical MCP command router.
139139
Sub commands are routed to MCP servers that require specific fields inside the "parameters" object.
140140
To invoke a command, set "command" and wrap its args in "parameters".
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<!-- cspell:ignore externaldata SRCH -->
2+
3+
# Basic and Auxiliary log search
4+
5+
`monitor workspace log search` (`monitor_workspace_log_search`) queries one primary Basic or Auxiliary table in a Log Analytics workspace. Existing Analytics query tools are unchanged.
6+
7+
## Why a separate tool
8+
9+
Analytics queries use `/v1/workspaces/{workspaceId}/query`. Basic and Auxiliary queries use `/v1/workspaces/{workspaceId}/search`, with workspace-only scope and a narrower KQL language.
10+
11+
Keeping separate tools avoids guessing the primary table from arbitrary KQL or adding metadata requests to existing Analytics queries. There is no automatic fallback between endpoints.
12+
13+
## Inputs and limits
14+
15+
| Option | Meaning |
16+
| --- | --- |
17+
| `subscription` | ID or name. If omitted, the standard resolver uses the configured default. |
18+
| `resource-group` | Required to identify the workspace unambiguously. |
19+
| `workspace` | Required workspace name; ARM supplies its customer GUID. |
20+
| `table` | Required Basic or Auxiliary table name, validated as an ASCII identifier. |
21+
| `query` | Required KQL pipeline beginning with `\|`, without the primary table name. |
22+
| `timespan` | Required positive ISO 8601 duration or RFC 3339 `start/end` interval, at most 30 days. Calendar years and months are not accepted. |
23+
| `limit` | Maximum returned rows: 1-100, default 20. |
24+
| `tenant` | Optional tenant ID or name. |
25+
26+
Basic queries cannot start more than 30 days ago. Auxiliary supports older retained data, but this tool limits each call to a 30-day interval. Query cost depends on data scanned across that interval, not the returned row limit.
27+
28+
## Query construction
29+
30+
The service builds `<table> <pipeline> | take <limit>`. It always appends the final `take`, including when the supplied pipeline already has a row limit.
31+
32+
The validator rejects multiple statements, comments, source functions, nested tabular pipelines, and unsupported operators such as `join`, `find`, `search`, `externaldata`, and `invoke`. It is not a KQL parser; Azure checks the remaining syntax and semantics. Azure-supported `union` and `lookup` enrichment from Analytics tables remains available. The result's `table` and `plan` fields identify the primary table, not every enrichment source.
33+
34+
## Table-plan checks
35+
36+
Before querying logs, the service reads the workspace and table through the ARM SDK:
37+
38+
1. Missing resources return 404.
39+
2. Analytics or other unsupported plans return 409 with guidance to use `monitor_workspace_log_query`, even if their plan-change timestamp is absent.
40+
3. A missing plan, or missing or invalid transition metadata on a Basic or Auxiliary table, returns 502.
41+
4. Ranges starting before `LastPlanModifiedDate` return 409 with the supported boundary. This prevents a query spanning different access behavior from appearing complete.
42+
43+
If the plan changes after the metadata read, the service error is returned without retrying through another endpoint.
44+
45+
## Results and failures
46+
47+
Results contain typed `columns`, positional `rows`, `rowCount`, `limit`, `isPartial`, and `error`. Numbers, booleans, nulls, and dynamic JSON keep their types.
48+
49+
An Azure `PartialError` retains usable rows with `isPartial: true` and sanitized error details. Fatal errors, malformed responses, invalid row shapes, and responses exceeding 1 MiB return an error rather than silently dropping data. HTTP 204 returns an empty result. `isPartial` reflects service-reported incompleteness; the row limit still applies to successful results.
50+
51+
There are no application retries, pagination, caching, or parallel queries. Throttling returns 429 with retry guidance. A linked cancellation token bounds both the HTTP request and response-body read.
52+
53+
## Authentication and cloud support
54+
55+
The service uses the repository's per-request Azure credential provider in both stdio and HTTP modes. It requires workspace read, table metadata read, and log query permissions. Callers cannot supply tokens, endpoints, or workspace customer GUIDs.
56+
57+
Only the documented public-cloud `/search` endpoint is enabled. Other clouds return an unsupported-cloud error before network access. Endpoint fallback is intentionally absent: retrying a query on another host could repeat a billable scan.
58+
59+
## Server discovery modes
60+
61+
| Server start configuration | Exposed tool | Routed command |
62+
| --- | --- | --- |
63+
| `--mode all --namespace monitor` | `monitor_workspace_log_search` | Direct tool call |
64+
| `--tool monitor_workspace_log_search` | `monitor_workspace_log_search` | Direct tool call |
65+
| `--namespace monitor` (default namespace mode) | `monitor` | `monitor_workspace_log_search` |
66+
| `--mode namespace --namespace monitor` | `monitor` | `monitor_workspace_log_search` |
67+
| `--mode single --namespace monitor` | `azure` | Tool `monitor`, command `monitor_workspace_log_search` |
68+
| `--mode consolidated --namespace monitor` | `get_azure_resource_and_app_health_status` | `get_azure_resource_and_app_health_status_monitor_workspace_log_search` |
69+
70+
`--tool` and `--namespace` cannot be combined. Routers accept the child arguments under `parameters`; `learn: true` lists their exact command names.
71+
72+
Structured output is opt-in with `--structured-output-mode duplicated` or `compact`. Direct mode advertises `WorkspaceLogSearchResult`; namespace and consolidated modes use the shared `tool-result` envelope. Single mode wraps the complete downstream MCP call result and forwards the output setting to its child server. See [output-schema conventions](../output-schema-migration.md).
73+
74+
## Search jobs are separate
75+
76+
Search jobs create persistent `*_SRCH` tables and can run for up to 24 hours. They require write permissions, incur ingestion costs, and need polling and cleanup. This synchronous read-only tool does not start search jobs.
77+
78+
## References
79+
80+
- [Query data in a Basic and Auxiliary table](https://learn.microsoft.com/azure/azure-monitor/logs/basic-logs-query)
81+
- [Access the Azure Monitor Log Analytics API](https://learn.microsoft.com/azure/azure-monitor/logs/api/access-api)
82+
- [Log Analytics API response format](https://learn.microsoft.com/azure/azure-monitor/logs/api/response-format)
83+
- [Azure Monitor service limits: log queries and language](https://learn.microsoft.com/azure/azure-monitor/fundamentals/service-limits#log-queries-and-language)
84+
- [Configure a table plan](https://learn.microsoft.com/azure/azure-monitor/logs/logs-table-plans)
85+
- [Run search jobs in Azure Monitor](https://learn.microsoft.com/azure/azure-monitor/logs/search-jobs)

servers/Azure.Mcp.Server/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,7 +1188,8 @@ Example prompts that generate Azure CLI commands:
11881188

11891189
### 📊 Azure Monitor
11901190

1191-
* "Query my Log Analytics workspace"
1191+
* "Query an Analytics table in my Log Analytics workspace"
1192+
* "Search a Basic or Auxiliary table in my Log Analytics workspace over the last day"
11921193
* "List my Azure Monitor Health Models"
11931194
* "Get details for my Azure Monitor Health Model 'my-health-model'"
11941195

@@ -1343,7 +1344,7 @@ The Azure MCP Server provides tools for interacting with **44+ Azure service are
13431344
- 🗃️ **Azure Managed Lustre** - High-performance Lustre filesystem operations
13441345
- 🏪 **Azure Marketplace** - Product discovery
13451346
- 🔄 **Azure Migrate** - Platform Landing Zone generation and modification guidance
1346-
- 📈 **Azure Monitor** - Logging, metrics, health models, health monitoring, and instrumentation onboarding/migration workflow for local applications
1347+
- 📈 **Azure Monitor** - Log queries, Basic and Auxiliary table search, metrics, health models, health monitoring, and instrumentation onboarding/migration workflow for local applications
13471348
- ⚖️ **Azure Policy** - Policies set to enforce organizational standards
13481349
- ⚙️ **Azure Native ISV Services** - Third-party integrations
13491350
- 🛡️ **Azure Quick Review CLI** - Compliance scanning
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
changes:
2+
- section: "Features Added"
3+
description: "Added `monitor workspace log search` for synchronous, bounded searches of Basic and Auxiliary Log Analytics tables."
4+
- section: "Bugs Fixed"
5+
description: "Preserved configured consolidated-tool descriptions and structured output when single mode starts child namespace servers."
6+
- section: "Bugs Fixed"
7+
description: "Corrected Analytics-table routing guidance when Log Analytics table metadata omits the plan-change timestamp."
8+
- section: "Bugs Fixed"
9+
description: "Fixed log search playback tests to use sanitized workspace and column metadata."

servers/Azure.Mcp.Server/docs/azmcp-commands.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3319,6 +3319,34 @@ azmcp monitor workspace log query --subscription <subscription> \
33193319
--workspace <workspace> \
33203320
--table "AppEvents_CL" \
33213321
--query "| order by TimeGenerated desc"
3322+
3323+
# Search a Basic or Auxiliary table in a Log Analytics workspace.
3324+
# Use workspace log query for Analytics tables.
3325+
# --query must begin with '|' and omit the primary table name.
3326+
# The server binds --table and caps output at --limit (default 20, maximum 100).
3327+
# --timespan is a positive ISO 8601 duration (such as "P1D") or a closed
3328+
# RFC 3339 start/end interval, up to 30 days. Basic queries cover only the last 30 days.
3329+
# Results preserve column types and flag service-reported partial results.
3330+
# Scan cost depends on ingested volume across --timespan, not --limit.
3331+
# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
3332+
azmcp monitor workspace log search --subscription <subscription> \
3333+
--resource-group <resource-group> \
3334+
--workspace <workspace> \
3335+
--table <table> \
3336+
--query <search-pipeline> \
3337+
--timespan <timespan> \
3338+
[--limit <limit>] \
3339+
[--tenant <tenant>]
3340+
3341+
# Search the last day of a Basic or Auxiliary table for error records
3342+
# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
3343+
azmcp monitor workspace log search --subscription <subscription> \
3344+
--resource-group <resource-group> \
3345+
--workspace <workspace> \
3346+
--table "ContainerLogV2" \
3347+
--query "| where LogLevel == 'error' | project TimeGenerated, LogMessage" \
3348+
--timespan "P1D" \
3349+
--limit 50
33223350
```
33233351
33243352
#### Health Models

0 commit comments

Comments
 (0)