mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
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:
@@ -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.'),
|
||||
|
||||
Reference in New Issue
Block a user