Add the diagnostics core and report model for MCP health, scope, and config analysis. This creates the structured report used by both text and JSON doctor output. Co-Authored-By: Claude <noreply@anthropic.com>
696 lines
21 KiB
TypeScript
696 lines
21 KiB
TypeScript
import type { ValidationError } from '../../utils/settings/validation.js'
|
|
import { clearServerCache, connectToServer } from './client.js'
|
|
import {
|
|
getAllMcpConfigs,
|
|
getMcpConfigsByScope,
|
|
isMcpServerDisabled,
|
|
} from './config.js'
|
|
import type {
|
|
ConfigScope,
|
|
ScopedMcpServerConfig,
|
|
} from './types.js'
|
|
import { describeMcpConfigFilePath, getProjectMcpServerStatus } from './utils.js'
|
|
|
|
export type McpDoctorSeverity = 'info' | 'warn' | 'error'
|
|
export type McpDoctorScopeFilter = 'local' | 'project' | 'user' | 'enterprise'
|
|
|
|
export type McpDoctorFinding = {
|
|
blocking: boolean
|
|
code: string
|
|
message: string
|
|
remediation?: string
|
|
scope?: string
|
|
serverName?: string
|
|
severity: McpDoctorSeverity
|
|
sourcePath?: string
|
|
}
|
|
|
|
export type McpDoctorLiveCheck = {
|
|
attempted: boolean
|
|
durationMs?: number
|
|
error?: string
|
|
result?: 'connected' | 'needs-auth' | 'failed' | 'pending' | 'disabled' | 'skipped'
|
|
}
|
|
|
|
export type McpDoctorDefinition = {
|
|
name: string
|
|
sourceType:
|
|
| 'local'
|
|
| 'project'
|
|
| 'user'
|
|
| 'enterprise'
|
|
| 'managed'
|
|
| 'plugin'
|
|
| 'claudeai'
|
|
| 'dynamic'
|
|
| 'internal'
|
|
sourcePath?: string
|
|
transport?: string
|
|
runtimeVisible: boolean
|
|
runtimeActive: boolean
|
|
pendingApproval?: boolean
|
|
disabled?: boolean
|
|
}
|
|
|
|
export type McpDoctorServerReport = {
|
|
serverName: string
|
|
requestedByUser: boolean
|
|
definitions: McpDoctorDefinition[]
|
|
liveCheck: McpDoctorLiveCheck
|
|
findings: McpDoctorFinding[]
|
|
}
|
|
|
|
export type McpDoctorDependencies = {
|
|
getAllMcpConfigs: typeof getAllMcpConfigs
|
|
getMcpConfigsByScope: typeof getMcpConfigsByScope
|
|
getProjectMcpServerStatus: typeof getProjectMcpServerStatus
|
|
isMcpServerDisabled: typeof isMcpServerDisabled
|
|
describeMcpConfigFilePath: typeof describeMcpConfigFilePath
|
|
connectToServer: typeof connectToServer
|
|
clearServerCache: typeof clearServerCache
|
|
}
|
|
|
|
export type McpDoctorReport = {
|
|
generatedAt: string
|
|
targetName?: string
|
|
scopeFilter?: McpDoctorScopeFilter
|
|
configOnly: boolean
|
|
summary: {
|
|
totalReports: number
|
|
healthy: number
|
|
warnings: number
|
|
blocking: number
|
|
}
|
|
findings: McpDoctorFinding[]
|
|
servers: McpDoctorServerReport[]
|
|
}
|
|
|
|
const DEFAULT_DEPENDENCIES: McpDoctorDependencies = {
|
|
getAllMcpConfigs,
|
|
getMcpConfigsByScope,
|
|
getProjectMcpServerStatus,
|
|
isMcpServerDisabled,
|
|
describeMcpConfigFilePath,
|
|
connectToServer,
|
|
clearServerCache,
|
|
}
|
|
|
|
export function buildEmptyDoctorReport(options: {
|
|
configOnly: boolean
|
|
scopeFilter?: McpDoctorScopeFilter
|
|
targetName?: string
|
|
}): McpDoctorReport {
|
|
return {
|
|
generatedAt: new Date().toISOString(),
|
|
targetName: options.targetName,
|
|
scopeFilter: options.scopeFilter,
|
|
configOnly: options.configOnly,
|
|
summary: {
|
|
totalReports: 0,
|
|
healthy: 0,
|
|
warnings: 0,
|
|
blocking: 0,
|
|
},
|
|
findings: [],
|
|
servers: [],
|
|
}
|
|
}
|
|
|
|
function getFindingCode(error: ValidationError): string {
|
|
if (error.message === 'MCP config is not a valid JSON') {
|
|
return 'config.invalid_json'
|
|
}
|
|
if (error.message.startsWith('Missing environment variables:')) {
|
|
return 'config.missing_env_vars'
|
|
}
|
|
if (error.message.includes("Windows requires 'cmd /c' wrapper to execute npx")) {
|
|
return 'config.windows_npx_wrapper_required'
|
|
}
|
|
if (error.message === 'Does not adhere to MCP server configuration schema') {
|
|
return 'config.invalid_schema'
|
|
}
|
|
return 'config.validation_error'
|
|
}
|
|
|
|
function getSeverity(error: ValidationError): McpDoctorSeverity {
|
|
const severity = error.mcpErrorMetadata?.severity
|
|
if (severity === 'fatal') {
|
|
return 'error'
|
|
}
|
|
if (severity === 'warning') {
|
|
return 'warn'
|
|
}
|
|
return 'warn'
|
|
}
|
|
|
|
export function findingsFromValidationErrors(
|
|
validationErrors: ValidationError[],
|
|
): McpDoctorFinding[] {
|
|
return validationErrors.map(error => {
|
|
const severity = getSeverity(error)
|
|
return {
|
|
blocking: severity === 'error',
|
|
code: getFindingCode(error),
|
|
message: error.message,
|
|
remediation: error.suggestion,
|
|
scope: error.mcpErrorMetadata?.scope,
|
|
serverName: error.mcpErrorMetadata?.serverName,
|
|
severity,
|
|
sourcePath: error.file,
|
|
}
|
|
})
|
|
}
|
|
|
|
function splitValidationFindings(validationFindings: McpDoctorFinding[]): {
|
|
globalFindings: McpDoctorFinding[]
|
|
serverFindingsByName: Map<string, McpDoctorFinding[]>
|
|
} {
|
|
const globalFindings: McpDoctorFinding[] = []
|
|
const serverFindingsByName = new Map<string, McpDoctorFinding[]>()
|
|
|
|
for (const finding of validationFindings) {
|
|
if (!finding.serverName) {
|
|
globalFindings.push(finding)
|
|
continue
|
|
}
|
|
|
|
const findings = serverFindingsByName.get(finding.serverName) ?? []
|
|
findings.push(finding)
|
|
serverFindingsByName.set(finding.serverName, findings)
|
|
}
|
|
|
|
return {
|
|
globalFindings,
|
|
serverFindingsByName,
|
|
}
|
|
}
|
|
|
|
function getSourceType(config: ScopedMcpServerConfig): McpDoctorDefinition['sourceType'] {
|
|
if (config.scope === 'claudeai') {
|
|
return 'claudeai'
|
|
}
|
|
if (config.scope === 'dynamic') {
|
|
return config.pluginSource ? 'plugin' : 'dynamic'
|
|
}
|
|
if (config.scope === 'managed') {
|
|
return 'managed'
|
|
}
|
|
return config.scope
|
|
}
|
|
|
|
function getTransport(config: ScopedMcpServerConfig): string {
|
|
return config.type ?? 'stdio'
|
|
}
|
|
|
|
function getConfigSignature(config: ScopedMcpServerConfig): string {
|
|
switch (config.type) {
|
|
case 'sse':
|
|
case 'http':
|
|
case 'ws':
|
|
case 'claudeai-proxy':
|
|
return `${config.scope}:${config.type}:${config.url}`
|
|
case 'sdk':
|
|
return `${config.scope}:${config.type}:${config.name}`
|
|
default:
|
|
return `${config.scope}:${config.type ?? 'stdio'}:${config.command}:${JSON.stringify(config.args ?? [])}`
|
|
}
|
|
}
|
|
|
|
function isSameDefinition(
|
|
config: ScopedMcpServerConfig,
|
|
activeConfig: ScopedMcpServerConfig | undefined,
|
|
): boolean {
|
|
if (!activeConfig) {
|
|
return false
|
|
}
|
|
return getSourceType(config) === getSourceType(activeConfig) && getConfigSignature(config) === getConfigSignature(activeConfig)
|
|
}
|
|
|
|
function buildScopeDefinitions(
|
|
name: string,
|
|
scope: ConfigScope,
|
|
servers: Record<string, ScopedMcpServerConfig>,
|
|
activeConfig: ScopedMcpServerConfig | undefined,
|
|
deps: McpDoctorDependencies,
|
|
): McpDoctorDefinition[] {
|
|
const config = servers[name]
|
|
if (!config) {
|
|
return []
|
|
}
|
|
|
|
const pendingApproval =
|
|
scope === 'project' ? deps.getProjectMcpServerStatus(name) === 'pending' : false
|
|
const disabled = deps.isMcpServerDisabled(name)
|
|
const runtimeActive = !disabled && isSameDefinition(config, activeConfig)
|
|
|
|
return [
|
|
{
|
|
name,
|
|
sourceType: getSourceType(config),
|
|
sourcePath: deps.describeMcpConfigFilePath(scope),
|
|
transport: getTransport(config),
|
|
runtimeVisible: runtimeActive,
|
|
runtimeActive,
|
|
pendingApproval,
|
|
disabled,
|
|
},
|
|
]
|
|
}
|
|
|
|
function shouldIncludeScope(
|
|
scope: ConfigScope,
|
|
scopeFilter: McpDoctorScopeFilter | undefined,
|
|
): boolean {
|
|
if (!scopeFilter) {
|
|
return scope === 'enterprise' || scope === 'local' || scope === 'project' || scope === 'user'
|
|
}
|
|
return scope === scopeFilter
|
|
}
|
|
|
|
function getValidationErrorsForSelectedScopes(
|
|
scopeResults: {
|
|
enterprise: ReturnType<McpDoctorDependencies['getMcpConfigsByScope']>
|
|
local: ReturnType<McpDoctorDependencies['getMcpConfigsByScope']>
|
|
project: ReturnType<McpDoctorDependencies['getMcpConfigsByScope']>
|
|
user: ReturnType<McpDoctorDependencies['getMcpConfigsByScope']>
|
|
},
|
|
scopeFilter: McpDoctorScopeFilter | undefined,
|
|
): ValidationError[] {
|
|
return [
|
|
...(shouldIncludeScope('enterprise', scopeFilter) ? scopeResults.enterprise.errors : []),
|
|
...(shouldIncludeScope('local', scopeFilter) ? scopeResults.local.errors : []),
|
|
...(shouldIncludeScope('project', scopeFilter) ? scopeResults.project.errors : []),
|
|
...(shouldIncludeScope('user', scopeFilter) ? scopeResults.user.errors : []),
|
|
]
|
|
}
|
|
|
|
function buildObservedDefinition(
|
|
name: string,
|
|
activeConfig: ScopedMcpServerConfig,
|
|
options?: {
|
|
disabled?: boolean
|
|
runtimeActive?: boolean
|
|
runtimeVisible?: boolean
|
|
},
|
|
): McpDoctorDefinition {
|
|
return {
|
|
name,
|
|
sourceType: getSourceType(activeConfig),
|
|
sourcePath:
|
|
getSourceType(activeConfig) === 'plugin'
|
|
? `plugin:${activeConfig.pluginSource ?? 'unknown'}`
|
|
: getSourceType(activeConfig) === 'claudeai'
|
|
? 'claude.ai'
|
|
: activeConfig.scope,
|
|
transport: getTransport(activeConfig),
|
|
runtimeVisible: options?.runtimeVisible ?? true,
|
|
runtimeActive: options?.runtimeActive ?? true,
|
|
disabled: options?.disabled ?? false,
|
|
}
|
|
}
|
|
|
|
function hasDefinitionForRuntimeSource(
|
|
definitions: McpDoctorDefinition[],
|
|
runtimeConfig: ScopedMcpServerConfig,
|
|
deps: McpDoctorDependencies,
|
|
): boolean {
|
|
const runtimeSourceType = getSourceType(runtimeConfig)
|
|
const runtimeSourcePath =
|
|
runtimeSourceType === 'plugin'
|
|
? `plugin:${runtimeConfig.pluginSource ?? 'unknown'}`
|
|
: runtimeSourceType === 'claudeai'
|
|
? 'claude.ai'
|
|
: deps.describeMcpConfigFilePath(runtimeConfig.scope)
|
|
|
|
return definitions.some(
|
|
definition =>
|
|
definition.sourceType === runtimeSourceType &&
|
|
definition.sourcePath === runtimeSourcePath &&
|
|
definition.transport === getTransport(runtimeConfig),
|
|
)
|
|
}
|
|
|
|
function buildShadowingFindings(definitions: McpDoctorDefinition[]): McpDoctorFinding[] {
|
|
const userEditable = definitions.filter(definition =>
|
|
definition.sourceType === 'local' ||
|
|
definition.sourceType === 'project' ||
|
|
definition.sourceType === 'user' ||
|
|
definition.sourceType === 'enterprise',
|
|
)
|
|
|
|
if (userEditable.length <= 1) {
|
|
return []
|
|
}
|
|
|
|
const active = userEditable.find(definition => definition.runtimeActive) ?? userEditable[0]
|
|
return [
|
|
{
|
|
blocking: false,
|
|
code: 'duplicate.same_name_multiple_scopes',
|
|
message: `Server is defined in multiple config scopes; active source is ${active.sourceType}`,
|
|
remediation: 'Remove or rename one of the duplicate definitions to avoid confusion.',
|
|
serverName: active.name,
|
|
severity: 'warn',
|
|
},
|
|
{
|
|
blocking: false,
|
|
code: 'scope.shadowed',
|
|
message: `${active.name} has shadowed definitions in lower-precedence config scopes.`,
|
|
remediation: 'Inspect the other definitions and remove the ones you no longer want to keep.',
|
|
serverName: active.name,
|
|
severity: 'warn',
|
|
},
|
|
]
|
|
}
|
|
|
|
function buildStateFindings(definitions: McpDoctorDefinition[]): McpDoctorFinding[] {
|
|
const findings: McpDoctorFinding[] = []
|
|
|
|
for (const definition of definitions) {
|
|
if (definition.pendingApproval) {
|
|
findings.push({
|
|
blocking: false,
|
|
code: 'state.pending_project_approval',
|
|
message: `${definition.name} is declared in project config but pending project approval.`,
|
|
remediation: 'Approve the server in the project MCP approval flow before expecting it to become active.',
|
|
scope: 'project',
|
|
serverName: definition.name,
|
|
severity: 'warn',
|
|
sourcePath: definition.sourcePath,
|
|
})
|
|
}
|
|
|
|
if (definition.disabled) {
|
|
findings.push({
|
|
blocking: false,
|
|
code: 'state.disabled',
|
|
message: `${definition.name} is currently disabled.`,
|
|
remediation: 'Re-enable the server before expecting it to be available at runtime.',
|
|
serverName: definition.name,
|
|
severity: 'warn',
|
|
sourcePath: definition.sourcePath,
|
|
})
|
|
}
|
|
}
|
|
|
|
return findings
|
|
}
|
|
|
|
function summarizeReport(report: McpDoctorReport): McpDoctorReport {
|
|
const allFindings = [...report.findings, ...report.servers.flatMap(server => server.findings)]
|
|
const blocking = allFindings.filter(finding => finding.blocking).length
|
|
const warnings = allFindings.filter(finding => finding.severity === 'warn').length
|
|
const healthy = report.servers.filter(
|
|
server =>
|
|
server.liveCheck.result === 'connected' &&
|
|
server.findings.every(finding => !finding.blocking && finding.severity !== 'warn'),
|
|
).length
|
|
|
|
return {
|
|
...report,
|
|
summary: {
|
|
totalReports: report.servers.length,
|
|
healthy,
|
|
warnings,
|
|
blocking,
|
|
},
|
|
}
|
|
}
|
|
|
|
async function getLiveCheck(
|
|
name: string,
|
|
activeConfig: ScopedMcpServerConfig | undefined,
|
|
configOnly: boolean,
|
|
definitions: McpDoctorDefinition[],
|
|
deps: McpDoctorDependencies,
|
|
): Promise<McpDoctorLiveCheck> {
|
|
if (configOnly) {
|
|
return { attempted: false, result: 'skipped' }
|
|
}
|
|
|
|
if (!activeConfig) {
|
|
if (definitions.some(definition => definition.pendingApproval)) {
|
|
return { attempted: false, result: 'pending' }
|
|
}
|
|
if (definitions.some(definition => definition.disabled)) {
|
|
return { attempted: false, result: 'disabled' }
|
|
}
|
|
return { attempted: false, result: 'skipped' }
|
|
}
|
|
|
|
const startedAt = Date.now()
|
|
const connection = await deps.connectToServer(name, activeConfig)
|
|
const durationMs = Date.now() - startedAt
|
|
|
|
try {
|
|
switch (connection.type) {
|
|
case 'connected':
|
|
return { attempted: true, result: 'connected', durationMs }
|
|
case 'needs-auth':
|
|
return { attempted: true, result: 'needs-auth', durationMs }
|
|
case 'disabled':
|
|
return { attempted: true, result: 'disabled', durationMs }
|
|
case 'pending':
|
|
return { attempted: true, result: 'pending', durationMs }
|
|
case 'failed':
|
|
return {
|
|
attempted: true,
|
|
result: 'failed',
|
|
durationMs,
|
|
error: connection.error,
|
|
}
|
|
}
|
|
} finally {
|
|
await deps.clearServerCache(name, activeConfig).catch(() => {
|
|
// Best-effort cleanup for diagnostic connections.
|
|
})
|
|
}
|
|
}
|
|
|
|
function buildLiveFindings(
|
|
name: string,
|
|
definitions: McpDoctorDefinition[],
|
|
liveCheck: McpDoctorLiveCheck,
|
|
): McpDoctorFinding[] {
|
|
const activeDefinition = definitions.find(definition => definition.runtimeActive)
|
|
|
|
if (liveCheck.result === 'needs-auth') {
|
|
return [
|
|
{
|
|
blocking: false,
|
|
code: 'auth.needs_auth',
|
|
message: `${name} requires authentication before it can be used.`,
|
|
remediation: 'Authenticate the server and then rerun the doctor command.',
|
|
serverName: name,
|
|
severity: 'warn',
|
|
sourcePath: activeDefinition?.sourcePath,
|
|
},
|
|
]
|
|
}
|
|
|
|
if (liveCheck.result === 'failed') {
|
|
const commandNotFound =
|
|
activeDefinition?.transport === 'stdio' &&
|
|
typeof liveCheck.error === 'string' &&
|
|
liveCheck.error.toLowerCase().includes('not found')
|
|
|
|
return [
|
|
{
|
|
blocking: true,
|
|
code: commandNotFound ? 'stdio.command_not_found' : 'health.failed',
|
|
message: liveCheck.error
|
|
? `${name} failed its live health check: ${liveCheck.error}`
|
|
: `${name} failed its live health check.`,
|
|
remediation: commandNotFound
|
|
? 'Verify the configured executable exists on PATH or use a full executable path.'
|
|
: 'Inspect the server configuration and retry the connection once the underlying problem is fixed.',
|
|
serverName: name,
|
|
severity: 'error',
|
|
sourcePath: activeDefinition?.sourcePath,
|
|
},
|
|
]
|
|
}
|
|
|
|
return []
|
|
}
|
|
|
|
async function buildServerReport(
|
|
name: string,
|
|
options: {
|
|
configOnly: boolean
|
|
requestedByUser: boolean
|
|
scopeFilter?: McpDoctorScopeFilter
|
|
},
|
|
validationFindingsByName: Map<string, McpDoctorFinding[]>,
|
|
deps: McpDoctorDependencies,
|
|
): Promise<McpDoctorServerReport> {
|
|
const scopeResults = {
|
|
enterprise: deps.getMcpConfigsByScope('enterprise'),
|
|
local: deps.getMcpConfigsByScope('local'),
|
|
project: deps.getMcpConfigsByScope('project'),
|
|
user: deps.getMcpConfigsByScope('user'),
|
|
}
|
|
const { servers: activeServers } = await deps.getAllMcpConfigs()
|
|
const serverDisabled = deps.isMcpServerDisabled(name)
|
|
const runtimeConfig = activeServers[name] ?? undefined
|
|
const activeConfig = serverDisabled ? undefined : runtimeConfig
|
|
|
|
const definitions = [
|
|
...(shouldIncludeScope('enterprise', options.scopeFilter)
|
|
? buildScopeDefinitions(name, 'enterprise', scopeResults.enterprise.servers, activeConfig, deps)
|
|
: []),
|
|
...(shouldIncludeScope('local', options.scopeFilter)
|
|
? buildScopeDefinitions(name, 'local', scopeResults.local.servers, activeConfig, deps)
|
|
: []),
|
|
...(shouldIncludeScope('project', options.scopeFilter)
|
|
? buildScopeDefinitions(name, 'project', scopeResults.project.servers, activeConfig, deps)
|
|
: []),
|
|
...(shouldIncludeScope('user', options.scopeFilter)
|
|
? buildScopeDefinitions(name, 'user', scopeResults.user.servers, activeConfig, deps)
|
|
: []),
|
|
]
|
|
|
|
const shouldAddObservedDefinition =
|
|
!!runtimeConfig &&
|
|
!hasDefinitionForRuntimeSource(definitions, runtimeConfig, deps) &&
|
|
((definitions.length === 0 && !options.scopeFilter) ||
|
|
(definitions.length > 0 && definitions.every(definition => !definition.runtimeActive)))
|
|
|
|
if (runtimeConfig && shouldAddObservedDefinition) {
|
|
definitions.push(
|
|
buildObservedDefinition(name, runtimeConfig, {
|
|
disabled: serverDisabled,
|
|
runtimeActive: !serverDisabled,
|
|
runtimeVisible: !serverDisabled,
|
|
}),
|
|
)
|
|
}
|
|
|
|
const visibleRuntimeConfig =
|
|
definitions.some(definition => definition.runtimeActive) || shouldAddObservedDefinition
|
|
? activeConfig
|
|
: undefined
|
|
|
|
const findings: McpDoctorFinding[] = [
|
|
...(validationFindingsByName.get(name) ?? []),
|
|
...buildShadowingFindings(definitions),
|
|
...buildStateFindings(definitions),
|
|
]
|
|
|
|
if (definitions.length === 0 && !shouldAddObservedDefinition) {
|
|
findings.push({
|
|
blocking: true,
|
|
code: 'state.not_found',
|
|
message: `${name} was not found in the selected MCP configuration sources.`,
|
|
remediation: 'Check the server name and scope, or add the MCP server before retrying.',
|
|
serverName: name,
|
|
severity: 'error',
|
|
})
|
|
}
|
|
|
|
const liveCheck = await getLiveCheck(name, visibleRuntimeConfig, options.configOnly, definitions, deps)
|
|
findings.push(...buildLiveFindings(name, definitions, liveCheck))
|
|
|
|
return {
|
|
serverName: name,
|
|
requestedByUser: options.requestedByUser,
|
|
definitions,
|
|
liveCheck,
|
|
findings,
|
|
}
|
|
}
|
|
|
|
function getServerNames(
|
|
scopeServers: Array<Record<string, ScopedMcpServerConfig>>,
|
|
activeServers: Record<string, ScopedMcpServerConfig>,
|
|
includeActiveServers: boolean,
|
|
): string[] {
|
|
const names = new Set<string>(includeActiveServers ? Object.keys(activeServers) : [])
|
|
for (const servers of scopeServers) {
|
|
for (const name of Object.keys(servers)) {
|
|
names.add(name)
|
|
}
|
|
}
|
|
return [...names].sort()
|
|
}
|
|
|
|
export async function doctorAllServers(
|
|
options: { configOnly: boolean; scopeFilter?: McpDoctorScopeFilter } = {
|
|
configOnly: false,
|
|
},
|
|
deps: McpDoctorDependencies = DEFAULT_DEPENDENCIES,
|
|
): Promise<McpDoctorReport> {
|
|
const report = buildEmptyDoctorReport(options)
|
|
const scopeResults = {
|
|
enterprise: deps.getMcpConfigsByScope('enterprise'),
|
|
local: deps.getMcpConfigsByScope('local'),
|
|
project: deps.getMcpConfigsByScope('project'),
|
|
user: deps.getMcpConfigsByScope('user'),
|
|
}
|
|
const validationFindings = findingsFromValidationErrors(
|
|
getValidationErrorsForSelectedScopes(scopeResults, options.scopeFilter),
|
|
)
|
|
const { globalFindings, serverFindingsByName } = splitValidationFindings(validationFindings)
|
|
const { servers: activeServers } = await deps.getAllMcpConfigs()
|
|
const names = getServerNames(
|
|
[
|
|
...(shouldIncludeScope('enterprise', options.scopeFilter) ? [scopeResults.enterprise.servers] : []),
|
|
...(shouldIncludeScope('local', options.scopeFilter) ? [scopeResults.local.servers] : []),
|
|
...(shouldIncludeScope('project', options.scopeFilter) ? [scopeResults.project.servers] : []),
|
|
...(shouldIncludeScope('user', options.scopeFilter) ? [scopeResults.user.servers] : []),
|
|
],
|
|
activeServers,
|
|
!options.scopeFilter,
|
|
)
|
|
|
|
const servers = await Promise.all(
|
|
names.map(name =>
|
|
buildServerReport(
|
|
name,
|
|
{
|
|
configOnly: options.configOnly,
|
|
requestedByUser: false,
|
|
scopeFilter: options.scopeFilter,
|
|
},
|
|
serverFindingsByName,
|
|
deps,
|
|
),
|
|
),
|
|
)
|
|
|
|
report.servers = servers
|
|
report.findings = globalFindings
|
|
return summarizeReport(report)
|
|
}
|
|
|
|
export async function doctorServer(
|
|
name: string,
|
|
options: { configOnly: boolean; scopeFilter?: McpDoctorScopeFilter },
|
|
deps: McpDoctorDependencies = DEFAULT_DEPENDENCIES,
|
|
): Promise<McpDoctorReport> {
|
|
const report = buildEmptyDoctorReport({ ...options, targetName: name })
|
|
const scopeResults = {
|
|
enterprise: deps.getMcpConfigsByScope('enterprise'),
|
|
local: deps.getMcpConfigsByScope('local'),
|
|
project: deps.getMcpConfigsByScope('project'),
|
|
user: deps.getMcpConfigsByScope('user'),
|
|
}
|
|
const validationFindings = findingsFromValidationErrors(
|
|
getValidationErrorsForSelectedScopes(scopeResults, options.scopeFilter),
|
|
)
|
|
const { globalFindings, serverFindingsByName } = splitValidationFindings(validationFindings)
|
|
const server = await buildServerReport(
|
|
name,
|
|
{
|
|
configOnly: options.configOnly,
|
|
requestedByUser: true,
|
|
scopeFilter: options.scopeFilter,
|
|
},
|
|
serverFindingsByName,
|
|
deps,
|
|
)
|
|
report.servers = [server]
|
|
report.findings = globalFindings
|
|
return summarizeReport(report)
|
|
}
|