/**
* mcp/skills/checkAdBinding.ts — check_ad_binding skill
*
* Checks whether the Mac is bound to an Active Directory domain and reports
* the binding status, domain name, and last successful authentication. Use
* when diagnosing AD login failures or password sync issues.
*
* Platform strategy
* -----------------
* darwin `dsconfigad -show` to get AD config,
* `dscl /Active\ Directory/ -list /` for domain list
* win32 PowerShell (Get-WmiObject Win32_ComputerSystem).PartOfDomain and
* [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/checkAdBinding.ts
*/
import * as os from "os" ;
import { exec } from "child_process" ;
import { promisify } from "util" ;
import { z } from "zod" ;
const execAsync = promisify (exec);
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "check_ad_binding" ,
description:
"Checks whether the Mac is bound to an Active Directory domain and reports " +
"the binding status, domain name, and last successful authentication. " +
"Use when diagnosing AD login failures or password sync issues." ,
riskLevel: "low" ,
destructive: false ,
requiresConsent: false ,
supportsDryRun: false ,
affectedScope: [ "user" ],
auditRequired: false ,
schema: {},
} as const ;
// -- Types --------------------------------------------------------------------
interface AdBindingInfo {
isBound : boolean ;
domain : string | null ;
domainController : string | null ;
lastBindCheck : string | null ;
errors : string [];
}
// -- PowerShell helper --------------------------------------------------------
async function runPS ( script : string ) : Promise < string > {
const encoded = Buffer. from (script, "utf16le" ). toString ( "base64" );
const { stdout } = await execAsync (
`powershell.exe -NoProfile -NonInteractive -EncodedCommand ${ encoded }` ,
{ maxBuffer: 10 * 1024 * 1024 },
);
return stdout. trim ();
}
// -- darwin implementation ----------------------------------------------------
async function checkAdBindingDarwin () : Promise < AdBindingInfo > {
const errors : string [] = [];
let isBound = false ;
let domain : string | null = null ;
let domainController : string | null = null ;
let lastBindCheck : string | null = null ;
// Run dsconfigad -show to get AD binding details
try {
const { stdout } = await execAsync (
"dsconfigad -show 2>&1" ,
{ maxBuffer: 2 * 1024 * 1024 },
);
if (stdout. includes ( "There is no Active Directory binding" )) {
isBound = false ;
} else {
// Parse domain
const domainMatch = stdout. match ( / Active Directory Domain \s * = \s * ( . + ) / i );
if (domainMatch) {
domain = domainMatch[ 1 ]. trim ();
isBound = true ;
}
// Parse domain controller
const dcMatch = stdout. match ( / Preferred Domain controller \s * = \s * ( . + ) / i );
if (dcMatch) domainController = dcMatch[ 1 ]. trim ();
// Parse last bind check from directory services logs if available
const bindingMatch = stdout. match ( / Computer Account \s * = \s * ( . + ) / i );
if (bindingMatch) lastBindCheck = `Computer account: ${ bindingMatch [ 1 ]. trim () }` ;
}
} catch (err) {
const msg = (err as Error ).message ?? "dsconfigad failed" ;
errors. push (msg);
}
// Confirm by listing /Active Directory domains
if (isBound) {
try {
const { stdout } = await execAsync (
`dscl "/Active Directory/" -list / 2>/dev/null` ,
{ maxBuffer: 1 * 1024 * 1024 , shell: "/bin/bash" },
);
const listed = stdout. trim (). split ( " \n " ). filter (Boolean);
if (listed. length > 0 && ! domain) {
domain = listed[ 0 ];
}
} catch (err) {
errors. push ( `dscl listing failed: ${ ( err as Error ). message }` );
}
}
return { isBound, domain, domainController, lastBindCheck, errors };
}
// -- win32 implementation -----------------------------------------------------
async function checkAdBindingWin32 () : Promise < AdBindingInfo > {
const errors : string [] = [];
const ps = `
$ErrorActionPreference = 'SilentlyContinue'
$cs = Get-WmiObject Win32_ComputerSystem
$isBound = [bool]$cs.PartOfDomain
$domain = if ($isBound) { $cs.Domain } else { $null }
$dc = $null
$lastCheck = $null
if ($isBound) {
try {
$d = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$dc = $d.PdcRoleOwner.Name
$lastCheck = (Get-Date).ToString('o')
} catch {
# domain unreachable
}
}
[PSCustomObject]@{
isBound = $isBound
domain = $domain
domainController = $dc
lastBindCheck = $lastCheck
errors = @()
} | ConvertTo-Json -Compress` . trim ();
try {
const raw = await runPS (ps);
const parsed = JSON . parse (raw) as AdBindingInfo ;
return parsed;
} catch (err) {
errors. push ((err as Error ).message);
return { isBound: false , domain: null , domainController: null , lastBindCheck: null , errors };
}
}
// -- Exported run function ----------------------------------------------------
export async function run ( _args : Record < string , never > = {}) {
const platform = os. platform ();
const info = platform === "win32"
? await checkAdBindingWin32 ()
: await checkAdBindingDarwin ();
return { platform, ... info };
}
// -- CLI smoke test -----------------------------------------------------------
if ( false ) {
run ({})
. then ( r => console. log ( JSON . stringify (r, null , 2 )))
. catch (( err : Error ) => { console. error (err.message); process. exit ( 1 ); });
}