feat(mcp): get_resource tool + description tightening

Workaround for clients (e.g. Claude Code's HTTP-MCP bridge in stateless
mode) that don't expose `resources/list` reliably. Verified with a paired
in-memory client that the server itself advertises `capabilities.resources`
and answers `resources/list` correctly — the breakage is on the client
side. Tools work everywhere, so a tool-call gateway is the pragmatic fix.

Resources now share a single source-of-truth (`resource-catalog.ts`) used
by both the MCP-protocol resource handlers and the new `get_resource(name)`
tool. Tool descriptions previously said "see the device_properties resource"
— updated to "call get_resource(name=...)" so the hint actually leads
somewhere usable.

Also tightens get_rule_stats description: the default window × number of
rules × per-day trend can easily exceed the response cap, so the doc now
recommends passing a tighter date range or ruleType filter for routine
queries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-05-07 16:40:01 +02:00
parent 629c12eb55
commit b4529d2c71
4 changed files with 159 additions and 111 deletions
@@ -0,0 +1,117 @@
/**
* Source-of-truth for the static resource catalogs (event types, device
* properties). Consumed by both `resources.ts` (MCP-protocol resources)
* and the `get_resource` tool (workaround for stateless-HTTP MCP clients
* whose resource discovery is broken).
*/
export const EVENT_TYPES_CATALOG = {
phase_events: [
'phase_transition',
'esp_state_change',
'completion_check',
'enrollment_complete',
'enrollment_failed',
'desktop_arrived',
],
app_events: [
'app_install_started',
'app_install_completed',
'app_install_failed',
'app_download_started',
'app_install_skipped',
],
network_events: ['network_state_change', 'network_connectivity_check'],
device_info_events: [
'os_info',
'hardware_spec',
'tpm_status',
'autopilot_profile',
'secureboot_status',
'bitlocker_status',
'network_adapters',
'network_interface_info',
'aad_join_status',
'enrollment_type_detected',
],
error_events: ['error_detected'],
vulnerability_events: ['software_inventory_analysis', 'vulnerability_report'],
other: [
'performance_snapshot',
'log_entry',
'gather_result',
'script_completed',
'script_failed',
'ime_agent_version',
],
} as const;
export const DEVICE_PROPERTIES_CATALOG = {
_usage: {
note: 'Use these keys in the deviceProperties parameter of search_sessions.',
booleanFormat: 'Use "True" or "False" (case-insensitive)',
numericOperators: 'Prefix value with >=, <=, >, or < for range filters (e.g. ">=8")',
arraySearch: 'For array properties, value is matched as substring in any array element',
},
tpm_status: {
'tpm_status.specVersion': { type: 'string', description: 'TPM specification version (e.g. "2.0", "1.2")' },
'tpm_status.manufacturerName': { type: 'string', description: 'TPM manufacturer (e.g. "INTC", "IFX")' },
'tpm_status.manufacturerVersion': { type: 'string', description: 'TPM firmware version' },
'tpm_status.isActivated': { type: 'boolean', description: 'TPM activation status' },
'tpm_status.isEnabled': { type: 'boolean', description: 'TPM enabled status' },
'tpm_status.isOwned': { type: 'boolean', description: 'TPM ownership status' },
},
secureboot_status: {
'secureboot_status.uefiSecureBootEnabled': { type: 'boolean', description: 'Secure Boot enabled' },
},
bitlocker_status: {
'bitlocker_status.systemDriveProtected': { type: 'boolean', description: 'System drive (C:) BitLocker protection' },
'bitlocker_status.volumes': { type: 'array', description: 'All BitLocker volumes — search by volume letter or protection status' },
},
autopilot_profile: {
'autopilot_profile.autopilotModeLabel': { type: 'string', description: 'Deployment mode (e.g. "User Driven (0)", "Self-Deploying (1)")' },
'autopilot_profile.domainJoinMethodLabel': { type: 'string', description: 'Join method (e.g. "Entra Join", "Hybrid Azure AD Join")' },
'autopilot_profile.CloudAssignedEspEnabled': { type: 'boolean', description: 'ESP enabled in Autopilot profile' },
'autopilot_profile.CloudAssignedDeviceName': { type: 'string', description: 'Cloud-assigned device name template' },
'autopilot_profile.CloudAssignedLanguage': { type: 'string', description: 'Cloud-assigned language/locale' },
},
hardware_spec: {
'hardware_spec.cpuName': { type: 'string', description: 'CPU model name (e.g. "Intel Core i7-1265U")' },
'hardware_spec.cpuCores': { type: 'number', description: 'Number of physical CPU cores' },
'hardware_spec.cpuLogicalProcessors': { type: 'number', description: 'Number of logical processors' },
'hardware_spec.ramTotalGB': { type: 'number', description: 'Total RAM in GB. Use >=N for minimum (e.g. ">=8")' },
'hardware_spec.ramSpeed': { type: 'string', description: 'RAM speed (e.g. "3200 MHz")' },
'hardware_spec.ramType': { type: 'string', description: 'RAM type (e.g. "DDR4", "LPDDR5")' },
'hardware_spec.diskCount': { type: 'number', description: 'Number of physical disks' },
'hardware_spec.hasSSD': { type: 'boolean', description: 'Has SSD or NVMe storage (computed)' },
'hardware_spec.disks': { type: 'array', description: 'Disk details — search for "NVMe", "SSD", disk model names' },
'hardware_spec.systemDriveFreeGB': { type: 'number', description: 'Free space on C: in GB' },
'hardware_spec.systemDriveTotalGB': { type: 'number', description: 'Total C: drive size in GB' },
'hardware_spec.biosVersion': { type: 'string', description: 'BIOS/UEFI version string' },
'hardware_spec.batteryPresent': { type: 'boolean', description: 'Device has a battery' },
'hardware_spec.batteryHealthPercent': { type: 'number', description: 'Battery health percentage' },
'hardware_spec.gpus': { type: 'array', description: 'GPU details — search for GPU model names' },
},
network_interface_info: {
'network_interface_info.connectionType': { type: 'string', description: 'Connection type: "WiFi" or "Ethernet"' },
'network_interface_info.linkSpeedMbps': { type: 'number', description: 'Link speed in Mbps' },
'network_interface_info.adapterName': { type: 'string', description: 'Network adapter name' },
'network_interface_info.macAddress': { type: 'string', description: 'MAC address of active adapter' },
},
aad_join_status: {
'aad_join_status.joinType': { type: 'string', description: 'Azure AD join type' },
'aad_join_status.tenantId': { type: 'string', description: 'Entra ID tenant' },
'aad_join_status.deviceId': { type: 'string', description: 'Entra device object ID' },
},
} as const;
export type ResourceName = 'event_types' | 'device_properties';
export function getResourceContent(name: ResourceName): unknown {
switch (name) {
case 'event_types':
return EVENT_TYPES_CATALOG;
case 'device_properties':
return DEVICE_PROPERTIES_CATALOG;
}
}
@@ -1,5 +1,12 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { EVENT_TYPES_CATALOG, DEVICE_PROPERTIES_CATALOG } from './resource-catalog.js';
/**
* MCP-protocol resources. Note that some clients (e.g. Claude Code's HTTP-MCP
* bridge in stateless mode) do not expose `resources/list` correctly — for
* those clients, use the `get_resource(name)` tool which returns the same
* data via a regular tool call.
*/
export function registerResources(server: McpServer): void {
server.resource(
'event_types',
@@ -13,50 +20,7 @@ export function registerResources(server: McpServer): void {
{
uri: 'autopilot://event-types',
mimeType: 'application/json',
text: JSON.stringify(
{
phase_events: [
'phase_transition',
'esp_state_change',
'completion_check',
'enrollment_complete',
'enrollment_failed',
'desktop_arrived',
],
app_events: [
'app_install_started',
'app_install_completed',
'app_install_failed',
'app_download_started',
'app_install_skipped',
],
network_events: ['network_state_change', 'network_connectivity_check'],
device_info_events: [
'os_info',
'hardware_spec',
'tpm_status',
'autopilot_profile',
'secureboot_status',
'bitlocker_status',
'network_adapters',
'network_interface_info',
'aad_join_status',
'enrollment_type_detected',
],
error_events: ['error_detected'],
vulnerability_events: ['software_inventory_analysis', 'vulnerability_report'],
other: [
'performance_snapshot',
'log_entry',
'gather_result',
'script_completed',
'script_failed',
'ime_agent_version',
],
},
null,
2
),
text: JSON.stringify(EVENT_TYPES_CATALOG, null, 2),
},
],
})
@@ -77,68 +41,7 @@ export function registerResources(server: McpServer): void {
{
uri: 'autopilot://device-properties',
mimeType: 'application/json',
text: JSON.stringify(
{
_usage: {
note: 'Use these keys in the deviceProperties parameter of search_sessions.',
booleanFormat: 'Use "True" or "False" (case-insensitive)',
numericOperators: 'Prefix value with >=, <=, >, or < for range filters (e.g. ">=8")',
arraySearch: 'For array properties, value is matched as substring in any array element',
},
tpm_status: {
'tpm_status.specVersion': { type: 'string', description: 'TPM specification version (e.g. "2.0", "1.2")' },
'tpm_status.manufacturerName': { type: 'string', description: 'TPM manufacturer (e.g. "INTC", "IFX")' },
'tpm_status.manufacturerVersion': { type: 'string', description: 'TPM firmware version' },
'tpm_status.isActivated': { type: 'boolean', description: 'TPM activation status' },
'tpm_status.isEnabled': { type: 'boolean', description: 'TPM enabled status' },
'tpm_status.isOwned': { type: 'boolean', description: 'TPM ownership status' },
},
secureboot_status: {
'secureboot_status.uefiSecureBootEnabled': { type: 'boolean', description: 'Secure Boot enabled' },
},
bitlocker_status: {
'bitlocker_status.systemDriveProtected': { type: 'boolean', description: 'System drive (C:) BitLocker protection' },
'bitlocker_status.volumes': { type: 'array', description: 'All BitLocker volumes — search by volume letter or protection status' },
},
autopilot_profile: {
'autopilot_profile.autopilotModeLabel': { type: 'string', description: 'Deployment mode (e.g. "User Driven (0)", "Self-Deploying (1)")' },
'autopilot_profile.domainJoinMethodLabel': { type: 'string', description: 'Join method (e.g. "Entra Join", "Hybrid Azure AD Join")' },
'autopilot_profile.CloudAssignedEspEnabled': { type: 'boolean', description: 'ESP enabled in Autopilot profile' },
'autopilot_profile.CloudAssignedDeviceName': { type: 'string', description: 'Cloud-assigned device name template' },
'autopilot_profile.CloudAssignedLanguage': { type: 'string', description: 'Cloud-assigned language/locale' },
},
hardware_spec: {
'hardware_spec.cpuName': { type: 'string', description: 'CPU model name (e.g. "Intel Core i7-1265U")' },
'hardware_spec.cpuCores': { type: 'number', description: 'Number of physical CPU cores' },
'hardware_spec.cpuLogicalProcessors': { type: 'number', description: 'Number of logical processors' },
'hardware_spec.ramTotalGB': { type: 'number', description: 'Total RAM in GB. Use >=N for minimum (e.g. ">=8")' },
'hardware_spec.ramSpeed': { type: 'string', description: 'RAM speed (e.g. "3200 MHz")' },
'hardware_spec.ramType': { type: 'string', description: 'RAM type (e.g. "DDR4", "LPDDR5")' },
'hardware_spec.diskCount': { type: 'number', description: 'Number of physical disks' },
'hardware_spec.hasSSD': { type: 'boolean', description: 'Has SSD or NVMe storage (computed)' },
'hardware_spec.disks': { type: 'array', description: 'Disk details — search for "NVMe", "SSD", disk model names' },
'hardware_spec.systemDriveFreeGB': { type: 'number', description: 'Free space on C: in GB' },
'hardware_spec.systemDriveTotalGB': { type: 'number', description: 'Total C: drive size in GB' },
'hardware_spec.biosVersion': { type: 'string', description: 'BIOS/UEFI version string' },
'hardware_spec.batteryPresent': { type: 'boolean', description: 'Device has a battery' },
'hardware_spec.batteryHealthPercent': { type: 'number', description: 'Battery health percentage' },
'hardware_spec.gpus': { type: 'array', description: 'GPU details — search for GPU model names' },
},
network_interface_info: {
'network_interface_info.connectionType': { type: 'string', description: 'Connection type: "WiFi" or "Ethernet"' },
'network_interface_info.linkSpeedMbps': { type: 'number', description: 'Link speed in Mbps' },
'network_interface_info.adapterName': { type: 'string', description: 'Network adapter name' },
'network_interface_info.macAddress': { type: 'string', description: 'MAC address of active adapter' },
},
aad_join_status: {
'aad_join_status.joinType': { type: 'string', description: 'Azure AD join type' },
'aad_join_status.tenantId': { type: 'string', description: 'Entra ID tenant' },
'aad_join_status.deviceId': { type: 'string', description: 'Entra device object ID' },
},
},
null,
2
),
text: JSON.stringify(DEVICE_PROPERTIES_CATALOG, null, 2),
},
],
})
@@ -2,6 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { apiFetch, buildQuery, followNextLink, pickGlobalOrTenantPath } from '../client.js';
import { withToolTelemetry } from '../telemetry.js';
import { getResourceContent } from '../resource-catalog.js';
import { READ_ONLY, READ_ONLY_OPEN, MAX_RESULT_SIZE_CHARS, toolResultText } from './shared.js';
import { toolError } from './error-handler.js';
@@ -519,7 +520,10 @@ export function registerAdminTools(server: McpServer): void {
'Get rule firing statistics for analyze and gather rules. Shows which rules fire most often, ' +
'their hit rates (fires/evaluations), and daily trends. Use to identify commonly triggered rules, ' +
'optimize rule definitions, or understand tenant-specific failure patterns. ' +
'Without tenantId returns global stats (cross-tenant). With tenantId returns tenant-specific stats.',
'Without tenantId returns global stats (cross-tenant). With tenantId returns tenant-specific stats. ' +
'NOTE: default window is 30 days × every rule × per-day trend rows — for 20+ rules the response ' +
'easily exceeds 70 KB and can trip the response cap. Pass a tighter `startDate`/`endDate` window ' +
'(7 days is usually plenty) and/or `ruleType` filter to keep responses lean.',
{
tenantId: z.string().optional().describe('Filter by tenant ID. Omit for global (cross-tenant) stats.'),
ruleType: z.enum(['analyze', 'gather']).optional().describe('Filter by rule type'),
@@ -544,4 +548,28 @@ export function registerAdminTools(server: McpServer): void {
}
})
);
// Tool: get_resource — returns the same content as the MCP `resources` would,
// but via a regular tool call. Workaround for clients (e.g. Claude Code's HTTP-MCP
// bridge in stateless mode) that don't expose `resources/list` reliably.
server.tool(
'get_resource',
'Returns the contents of a named static catalog resource. Use this when ' +
'the host MCP client cannot list/read MCP-protocol resources (common with ' +
'stateless HTTP MCP servers). Available names:\n' +
' - "event_types": catalog of valid eventType strings for search_sessions_by_event\n' +
' - "device_properties": catalog of dot-notation keys for the deviceProperties filter on search_sessions',
{
name: z.enum(['event_types', 'device_properties']).describe('Resource name'),
},
READ_ONLY_OPEN,
async (args) => withToolTelemetry('get_resource', async () => {
try {
const data = getResourceContent(args.name);
return toolResultText(data, MAX_RESULT_SIZE_CHARS.small);
} catch (error: unknown) {
return toolError('get_resource', args, error);
}
})
);
}
@@ -38,7 +38,7 @@ export function registerSessionTools(server: McpServer): void {
'Search enrollment sessions. Omit tenantId for cross-tenant search (Global Admin), or specify tenantId for single-tenant. ' +
'Basic properties (status, serial number, manufacturer, model, etc.) filter on the session index. ' +
'Use deviceProperties for ANY device hardware/config filter — keys use "eventType.propertyName" notation. ' +
'Consult the device_properties resource for available keys. ' +
'Consult the device_properties catalog (call get_resource(name="device_properties")) for available keys. ' +
'Examples: {"tpm_status.specVersion": "2.0"}, {"hardware_spec.ramTotalGB": ">=8"}, {"secureboot_status.uefiSecureBootEnabled": "True"}. ' +
'Array values are searched as substring match (e.g. disks containing "NVMe"). ' +
'For COUNTING / AGGREGATION queries (e.g. "how many V2 enrollments?", "how many failed in last 7 days?") ALWAYS pass ' +
@@ -78,7 +78,7 @@ export function registerSessionTools(server: McpServer): void {
'isUserDriven, isHybridJoin, agentVersion, imeAgentVersion, geoCountry.'),
deviceProperties: z.record(z.string(), z.string()).optional().describe(
'Dynamic device property filters. Keys use "eventType.propertyName" dot notation. ' +
'See the device_properties resource for all available keys and types. ' +
'See the device_properties catalog (call get_resource(name="device_properties")) for all available keys and types. ' +
'Values: exact match by default. Prefix with >=, <=, >, < for numeric ranges (e.g. ">=8"). ' +
'Booleans: use "True" or "False". Arrays: substring match in any element.'
),
@@ -116,14 +116,14 @@ export function registerSessionTools(server: McpServer): void {
server.tool(
'search_sessions_by_event',
'Find sessions that contain a specific event type (e.g. app install failure, phase transitions, errors). ' +
'Omit tenantId for cross-tenant search (Global Admin). Check the event_types resource for valid eventType values. ' +
'Omit tenantId for cross-tenant search (Global Admin). Check the event_types catalog (call get_resource(name="event_types")) for valid eventType values. ' +
'Use this to answer: which devices had a failed Teams install, which sessions had an error in DeviceSetup phase. ' +
'This endpoint is fully paginated — there is no truncation. The default pageSize=200 is tuned for typical ' +
'interactive queries; raise it (up to 1000) for full sweeps. For broad analysis, use pageSize=1000 and follow ' +
'nextLink repeatedly until absent. Pass the whole nextLink string as "continuation" so all backend-echoed query ' +
'params round-trip correctly.',
{
eventType: z.string().describe('Event type string — see event_types resource for valid values (e.g. "app_install_failed", "enrollment_failed")'),
eventType: z.string().describe('Event type string — see event_types catalog (call get_resource(name="event_types")) for valid values (e.g. "app_install_failed", "enrollment_failed")'),
tenantId: z.string().optional().describe('Tenant ID. Omit for cross-tenant search (Global Admin only).'),
pageSize: z.coerce.number().int().min(1).max(1000).optional().default(200)
.describe('Page size (1-1000, default 200). Returns this many sessions per call; follow nextLink to fetch more.'),