mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 16:03:26 +00:00
276 lines
7.0 KiB
TypeScript
276 lines
7.0 KiB
TypeScript
export type McpConfigFormat = 'workspace' | 'cursor' | 'claude'
|
|
|
|
export type McpConfigCandidate = {
|
|
format: McpConfigFormat
|
|
label: string
|
|
relativePath: string
|
|
serversPath: string[]
|
|
}
|
|
|
|
export type McpConfigDirectoryEntry = {
|
|
name: string
|
|
isDirectory: boolean
|
|
}
|
|
|
|
export type McpServerTransport = 'stdio' | 'http' | 'unknown'
|
|
export type McpServerStatus = 'enabled' | 'disabled' | 'invalid'
|
|
|
|
export type McpServerSummary = {
|
|
name: string
|
|
transport: McpServerTransport
|
|
status: McpServerStatus
|
|
command?: string
|
|
url?: string
|
|
env?: Record<string, string>
|
|
issue?: string
|
|
}
|
|
|
|
export type McpConfigInspection = {
|
|
candidate: McpConfigCandidate
|
|
exists: boolean
|
|
status: 'missing' | 'valid' | 'invalid'
|
|
servers: McpServerSummary[]
|
|
error?: string
|
|
}
|
|
|
|
export const MCP_CONFIG_CANDIDATES: McpConfigCandidate[] = [
|
|
{
|
|
format: 'workspace',
|
|
label: 'Workspace',
|
|
relativePath: '.mcp.json',
|
|
serversPath: ['mcpServers']
|
|
},
|
|
{
|
|
format: 'cursor',
|
|
label: 'Cursor',
|
|
relativePath: '.cursor/mcp.json',
|
|
serversPath: ['mcpServers']
|
|
},
|
|
{
|
|
format: 'claude',
|
|
label: 'Claude',
|
|
relativePath: '.claude.json',
|
|
serversPath: ['mcpServers']
|
|
},
|
|
{
|
|
format: 'claude',
|
|
label: 'Claude workspace',
|
|
relativePath: '.claude/mcp.json',
|
|
serversPath: ['mcpServers']
|
|
}
|
|
]
|
|
|
|
export const MCP_STARTER_CONFIG = `{
|
|
"mcpServers": {}
|
|
}
|
|
`
|
|
|
|
export function getMcpConfigParentDirs(
|
|
candidates: readonly McpConfigCandidate[] = MCP_CONFIG_CANDIDATES
|
|
): string[] {
|
|
return Array.from(
|
|
new Set(
|
|
candidates
|
|
.map((candidate) => getRelativeParentDir(candidate.relativePath))
|
|
.filter((parentDir) => parentDir !== '')
|
|
)
|
|
)
|
|
}
|
|
|
|
export function getMcpConfigCandidateParentDir(candidate: McpConfigCandidate): string {
|
|
return getRelativeParentDir(candidate.relativePath)
|
|
}
|
|
|
|
export function selectExistingMcpConfigCandidates(
|
|
entriesByRelativeDir: ReadonlyMap<string, readonly McpConfigDirectoryEntry[]>,
|
|
candidates: readonly McpConfigCandidate[] = MCP_CONFIG_CANDIDATES
|
|
): McpConfigCandidate[] {
|
|
return candidates.filter((candidate) => {
|
|
const parentDir = getRelativeParentDir(candidate.relativePath)
|
|
const basename = getRelativeBasename(candidate.relativePath)
|
|
const entries = entriesByRelativeDir.get(parentDir) ?? []
|
|
return entries.some((entry) => entry.name === basename && !entry.isDirectory)
|
|
})
|
|
}
|
|
|
|
export function canInspectLocalMcpConfigRoot(rootPath: string, isWindowsHost: boolean): boolean {
|
|
if (isWindowsHost) {
|
|
return true
|
|
}
|
|
return !/^(?:[A-Za-z]:[\\/]|[\\/]{2}[^\\/]+[\\/][^\\/]+)/.test(rootPath)
|
|
}
|
|
|
|
const SENSITIVE_ENV_KEY_PATTERN =
|
|
/(api[_-]?key|auth|bearer|cookie|credential|password|private[_-]?key|secret|session|token)/i
|
|
const SENSITIVE_ENV_VALUE_PATTERN =
|
|
/(sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,})/
|
|
|
|
export function inspectMcpConfigContent(
|
|
candidate: McpConfigCandidate,
|
|
content: string | null
|
|
): McpConfigInspection {
|
|
if (content === null) {
|
|
return { candidate, exists: false, status: 'missing', servers: [] }
|
|
}
|
|
|
|
let parsed: unknown
|
|
try {
|
|
parsed = JSON.parse(content)
|
|
} catch (error) {
|
|
return {
|
|
candidate,
|
|
exists: true,
|
|
status: 'invalid',
|
|
servers: [],
|
|
error: error instanceof Error ? error.message : 'Invalid JSON'
|
|
}
|
|
}
|
|
|
|
const rawServers = extractObjectAtPath(parsed, candidate.serversPath)
|
|
if (!rawServers) {
|
|
return { candidate, exists: true, status: 'valid', servers: [] }
|
|
}
|
|
|
|
return {
|
|
candidate,
|
|
exists: true,
|
|
status: 'valid',
|
|
servers: Object.entries(rawServers).map(([name, entry]) => summarizeMcpServer(name, entry))
|
|
}
|
|
}
|
|
|
|
export function maskMcpEnv(env: unknown): Record<string, string> | undefined {
|
|
if (!env || typeof env !== 'object' || Array.isArray(env)) {
|
|
return undefined
|
|
}
|
|
|
|
const masked: Record<string, string> = {}
|
|
for (const [key, rawValue] of Object.entries(env)) {
|
|
const value = typeof rawValue === 'string' ? rawValue : String(rawValue)
|
|
masked[key] =
|
|
SENSITIVE_ENV_KEY_PATTERN.test(key) || SENSITIVE_ENV_VALUE_PATTERN.test(value)
|
|
? '••••••••'
|
|
: value
|
|
}
|
|
return masked
|
|
}
|
|
|
|
function getRelativeParentDir(relativePath: string): string {
|
|
const normalizedPath = relativePath.replace(/\\/g, '/')
|
|
const separatorIndex = normalizedPath.lastIndexOf('/')
|
|
return separatorIndex === -1 ? '' : normalizedPath.slice(0, separatorIndex)
|
|
}
|
|
|
|
function getRelativeBasename(relativePath: string): string {
|
|
const normalizedPath = relativePath.replace(/\\/g, '/')
|
|
const separatorIndex = normalizedPath.lastIndexOf('/')
|
|
return separatorIndex === -1 ? normalizedPath : normalizedPath.slice(separatorIndex + 1)
|
|
}
|
|
|
|
function extractObjectAtPath(
|
|
value: unknown,
|
|
pathSegments: string[]
|
|
): Record<string, unknown> | null {
|
|
let current = value
|
|
for (const segment of pathSegments) {
|
|
if (!current || typeof current !== 'object' || Array.isArray(current)) {
|
|
return null
|
|
}
|
|
current = (current as Record<string, unknown>)[segment]
|
|
}
|
|
return current && typeof current === 'object' && !Array.isArray(current)
|
|
? (current as Record<string, unknown>)
|
|
: null
|
|
}
|
|
|
|
function summarizeMcpServer(name: string, entry: unknown): McpServerSummary {
|
|
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
return {
|
|
name,
|
|
transport: 'unknown',
|
|
status: 'invalid',
|
|
issue: 'Server entry must be an object.'
|
|
}
|
|
}
|
|
|
|
const raw = entry as Record<string, unknown>
|
|
const command = readCommand(raw)
|
|
const url = readUrl(raw)
|
|
const transport = resolveTransport(raw, command, url)
|
|
const enabled = raw.enabled !== false && raw.disabled !== true
|
|
const env = maskMcpEnv(raw.env)
|
|
|
|
if (transport === 'unknown') {
|
|
return {
|
|
name,
|
|
transport,
|
|
status: 'invalid',
|
|
env,
|
|
issue: 'Missing command or URL.'
|
|
}
|
|
}
|
|
|
|
if (transport === 'http' && !url) {
|
|
return {
|
|
name,
|
|
transport,
|
|
status: 'invalid',
|
|
env,
|
|
issue: 'Missing URL.'
|
|
}
|
|
}
|
|
|
|
if (transport === 'stdio' && !command) {
|
|
return {
|
|
name,
|
|
transport,
|
|
status: 'invalid',
|
|
env,
|
|
issue: 'Missing command.'
|
|
}
|
|
}
|
|
|
|
return {
|
|
name,
|
|
transport,
|
|
status: enabled ? 'enabled' : 'disabled',
|
|
command,
|
|
url,
|
|
env
|
|
}
|
|
}
|
|
|
|
function readCommand(raw: Record<string, unknown>): string | undefined {
|
|
if (typeof raw.command === 'string') {
|
|
return raw.command
|
|
}
|
|
if (Array.isArray(raw.command) && typeof raw.command[0] === 'string') {
|
|
return raw.command[0]
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function readUrl(raw: Record<string, unknown>): string | undefined {
|
|
if (typeof raw.url === 'string') {
|
|
return raw.url
|
|
}
|
|
if (typeof raw.httpUrl === 'string') {
|
|
return raw.httpUrl
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function resolveTransport(
|
|
raw: Record<string, unknown>,
|
|
command: string | undefined,
|
|
url: string | undefined
|
|
): McpServerTransport {
|
|
if (raw.type === 'http' || raw.type === 'remote' || url) {
|
|
return 'http'
|
|
}
|
|
if (raw.type === 'local' || command) {
|
|
return 'stdio'
|
|
}
|
|
return 'unknown'
|
|
}
|