#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { JamfApiClientHybrid } from './jamf-client-hybrid.js';
import { registerTools } from './tools/index.js';
import { registerResources } from './resources/index.js';
import { registerPrompts } from './prompts/index.js';
import { SkillsManager } from './skills/manager.js';
import { registerSkillsAsMCPTools } from './tools/skills-mcp-integration.js';
import { setupGlobalErrorHandlers } from './utils/error-handler.js';
import { createLogger } from './server/logger.js';
import { registerShutdownHandler, registerCommonHandlers } from './utils/shutdown-manager.js';
import { cleanupAuthMiddleware } from './server/auth-middleware.js';
import { cleanupAgentPool } from './utils/http-agent-pool.js';
const logger = createLogger('main');
// Environment variables
const JAMF_URL = process.env.JAMF_URL;
const JAMF_CLIENT_ID = process.env.JAMF_CLIENT_ID;
const JAMF_CLIENT_SECRET = process.env.JAMF_CLIENT_SECRET;
const JAMF_USERNAME = process.env.JAMF_USERNAME;
const JAMF_PASSWORD = process.env.JAMF_PASSWORD;
const READ_ONLY_MODE = process.env.JAMF_READ_ONLY === 'true';
// Validate configuration
if (!JAMF_URL) {
console.error('Missing required environment variable: JAMF_URL');
process.exit(1);
}
// Check for at least one auth method
const hasOAuth2 = !!(JAMF_CLIENT_ID && JAMF_CLIENT_SECRET);
const hasBasicAuth = !!(JAMF_USERNAME && JAMF_PASSWORD);
if (!hasOAuth2 && !hasBasicAuth) {
console.error('Missing authentication credentials. Please provide either:');
console.error(' - OAuth2: JAMF_CLIENT_ID and JAMF_CLIENT_SECRET');
console.error(' - Basic Auth: JAMF_USERNAME and JAMF_PASSWORD');
process.exit(1);
}
const server = new Server(
{
name: 'jamf-mcp-server',
version: '1.2.0',
},
{
capabilities: {
tools: {},
resources: {},
prompts: {},
},
instructions: `You are connected to a Jamf Pro MDM server managing Apple devices (macOS, iOS, iPadOS, tvOS).
## Quick Start — Common Questions
- "How's my fleet?" → Use \`getFleetOverview\` (single call replaces 4-6 individual calls)
- "Tell me about device X" → Use \`getDeviceFullProfile\` with name, serial, or ID
- "What's our security posture?" → Use \`getSecurityPosture\` for encryption, compliance, OS currency
- "How is policy X performing?" → Use \`getPolicyAnalysis\` with policy ID or name
## Tool Categories (by prefix)
- **Read-only (safe):** \`search*\`, \`list*\`, \`get*\`, \`check*\` — no side effects
- **Write/create:** \`create*\`, \`clone*\`, \`update*\`, \`set*\` — modifies configuration
- **Destructive (confirm required):** \`execute*\`, \`deploy*\`, \`send*\`, \`delete*\`, \`flush*\`, \`remove*\` — affects devices or deletes data
## Performance Tips
1. **Prefer compound tools** (\`getFleetOverview\`, \`getDeviceFullProfile\`, \`getSecurityPosture\`, \`getPolicyAnalysis\`) — they run parallel API calls internally
2. **Use \`getDevicesBatch\`** instead of calling \`getDeviceDetails\` in a loop
3. **Use resources** (jamf://reports/*) for pre-aggregated reports like compliance, encryption, OS versions
4. **Use \`getInventorySummary\`** for fleet-wide inventory stats without fetching individual devices
## Response Format
Many tools return enriched responses with:
- \`summary\`: Human-readable 1-2 sentence overview
- \`suggestedNextActions\`: Array of recommended follow-up tool calls
- \`data\`: The full API response data
- \`metadata\`: Result count, timestamp
## Important Notes
- All destructive tools require \`confirm: true\` parameter
- Device identifiers can be Jamf IDs, serial numbers, or device names (compound tools resolve automatically)
- The server supports both the Jamf Pro API and Classic API with automatic fallback`,
}
);
// Initialize Skills Manager
const skillsManager = new SkillsManager();
async function run() {
try {
logger.info('Starting Jamf MCP server with Skills...');
logger.info(`Jamf URL: ${JAMF_URL}`);
logger.info('Authentication methods available:');
if (hasOAuth2) {
logger.info(` ✅ OAuth2 (Jamf Pro API) - Client ID: ${JAMF_CLIENT_ID}`);
}
if (hasBasicAuth) {
logger.info(` ✅ Basic Auth (Bearer Token) - Username: ${JAMF_USERNAME}`);
}
logger.info(`Read-only mode: ${READ_ONLY_MODE}`);
logger.info('Skills integration: ✅ Enabled');
// Initialize the hybrid client
const jamfClient = new JamfApiClientHybrid({
baseUrl: JAMF_URL!,
clientId: JAMF_CLIENT_ID,
clientSecret: JAMF_CLIENT_SECRET,
username: JAMF_USERNAME,
password: JAMF_PASSWORD,
readOnlyMode: READ_ONLY_MODE,
// TLS/SSL configuration - only disable for development with self-signed certs
rejectUnauthorized: process.env.JAMF_ALLOW_INSECURE !== 'true',
});
// Register handlers
registerTools(server, jamfClient);
registerResources(server, jamfClient);
registerPrompts(server);
// Register skills as MCP tools
registerSkillsAsMCPTools(server, skillsManager, jamfClient);
// Start the server
const transport = new StdioServerTransport();
await server.connect(transport);
logger.info('Jamf MCP server started successfully with skills');
} catch (error) {
logger.error('Failed to initialize Jamf MCP server', { error });
process.exit(1);
}
}
// Setup global error handlers
setupGlobalErrorHandlers();
// Register common shutdown handlers
registerCommonHandlers();
// Register cleanup handlers
registerShutdownHandler('auth-cleanup', cleanupAuthMiddleware, 20);
registerShutdownHandler('agent-pool-cleanup', cleanupAgentPool, 20);
registerShutdownHandler('server-transport-close', async () => {
logger.info('Closing server transport...');
// Transport will be closed automatically
}, 40);
// Run the server
run().catch((error) => {
logger.error('Fatal error', { error });
process.exit(1);
});