#!/usr/bin/env node /** * Kubernetes MCP Server * Main entry point for the Model Context Protocol server. */ import { Resource, type Tool } from '@modelcontextprotocol/server'; import { serveStdio } from '@modelcontextprotocol/server/stdio'; import { MCPServer } from './server/MCPServer.js'; import { StreamableHttpRuntime } from './server/StreamableHttpRuntime.js'; import { loadTransportConfig } from './server/TransportConfig.js'; import { KubernetesToolsPlugin } from './plugins/KubernetesToolsPlugin.js'; import { HelmToolsPlugin } from './plugins/HelmToolsPlugin.js'; import { ArgoToolsPlugin } from './plugins/ArgoToolsPlugin.js'; import { ArgoCDToolsPlugin } from './plugins/ArgoCDToolsPlugin.js'; import { RunCodeTool } from './tools/RunCodeTool.js'; import { loadCodeModeConfig } from './utils/CodeModeConfig.js'; import { VERSION } from './version.js'; import { assertSensitiveApprovalDeploymentConfig, isGuardedToolName, normalizeQualifiedToolName, sensitiveToolApprovalGuard, withSensitiveToolAnnotations, } from './security/SensitiveToolApproval.js'; import { POD_EXEC_TOOL_NAME, PERMANENT_CODE_MODE_DENIALS, annotateReadOnlyTool, disabledToolDetails, } from './utils/ToolPolicy.js'; import { detectOptionalCapabilities, type OptionalCapabilities, } from './plugins/OptionalCapabilityDetector.js'; import { createArgoCDTool, createArgoTool, createHelmTool, directDomainOperations, executeDomainTool, } from './tools/DirectDomainTools.js'; export { VERSION }; type MCPMode = 'code' | 'tools' | 'all'; export function getMCPMode(): MCPMode { const mode = process.env.MCP_MODE?.toLowerCase(); if (mode === 'code' || mode === 'tools' || mode === 'all') { return mode; } return 'code'; } interface InternalCatalog { server: MCPServer; optional: OptionalCapabilities; } async function createInternalCatalog(): Promise { const internalServer = new MCPServer({ skipTransportErrorHandling: true, skipGracefulShutdown: true, }); await internalServer.loadPlugin(new KubernetesToolsPlugin()); await internalServer.loadPlugin(new HelmToolsPlugin()); const optional = await detectOptionalCapabilities({ context: process.env.MCP_KUBE_CONTEXT?.trim() || undefined, }); if (optional.argo) await internalServer.loadPlugin(new ArgoToolsPlugin()); if (optional.argocd) await internalServer.loadPlugin(new ArgoCDToolsPlugin()); return { server: internalServer, optional }; } async function configureCodeModeServer(server: MCPServer): Promise { const config = loadCodeModeConfig(); const { server: internalServer } = await createInternalCatalog(); const configurableDenials = new Set(config.disabledTools.map(normalizeQualifiedToolName)); const allDenials = new Set([...PERMANENT_CODE_MODE_DENIALS, ...configurableDenials]); const knownNames = new Set(internalServer.getTools().map((tool) => tool.name)); for (const toolName of configurableDenials) { if (!knownNames.has(toolName)) throw new Error(`Unknown code-mode disabled tool: ${toolName}`); } const toolExecutor = async (qualifiedName: string, args: unknown) => { const toolName = normalizeQualifiedToolName(qualifiedName); if (allDenials.has(toolName)) { throw new Error( isGuardedToolName(toolName) ? `Tool '${toolName}' cannot run inside run_code. Call the guarded top-level tool so the user can approve it.` : `Tool '${toolName}' is disabled inside run_code by MCP code-mode policy.`, ); } return internalServer.executeTool(toolName, args); }; const runCodeTool = new RunCodeTool(config.sandbox); runCodeTool.setDeniedTools(allDenials); runCodeTool.setDisabledToolDetails(disabledToolDetails(configurableDenials)); runCodeTool.setToolExecutor(toolExecutor); runCodeTool.setTools(internalServer.getTools()); server.registerTool(runCodeTool.tool, (params) => runCodeTool.execute(params)); const execTool = internalServer .getTools() .find((candidate) => candidate.name === POD_EXEC_TOOL_NAME); if (!execTool) throw new Error(`Required tool '${POD_EXEC_TOOL_NAME}' was not registered`); server.registerTool(withSensitiveToolAnnotations(execTool), (params, context) => sensitiveToolApprovalGuard.authorizeAndExecute(POD_EXEC_TOOL_NAME, params, context, () => internalServer.executeTool(POD_EXEC_TOOL_NAME, params), ), ); registerCodeModeResources(server, runCodeTool); } function registerCodeModeResources(server: MCPServer, runCodeTool: RunCodeTool): void { server.registerResource({ uri: 'file:///sys/global.d.ts', name: 'Global Type Definitions', mimeType: 'application/typescript', text: runCodeTool.generateGlobalDts(), } as Resource & { text: string }); server.registerPrompt({ name: 'code-mode', description: 'TypeScript API documentation for the run_code environment.', arguments: [], getMessages: async () => [ { role: 'user', content: { type: 'text', text: runCodeTool.getPromptContent() }, }, ], }); } async function configureToolsModeServer(server: MCPServer): Promise { const { server: internalServer, optional } = await createInternalCatalog(); const registerReadTool = (name: string, title: string) => { const tool = internalServer.getTools().find((candidate) => candidate.name === name); if (!tool) throw new Error(`Required internal tool '${name}' was not registered`); server.registerTool( { ...annotateReadOnlyTool(tool, title), outputSchema: tool.outputSchema ?? (name === 'kube_logs' ? ({ type: ['object', 'string'], description: 'Structured log records or requested text output.', } as Tool['outputSchema']) : ({ type: 'object', description: 'Machine-readable result from the Kubernetes API.', } as Tool['outputSchema'])), }, (params) => internalServer.executeTool(name, params), ); }; registerReadTool('kube_list', 'List Kubernetes Resources'); registerReadTool('kube_get', 'Get Kubernetes Resource'); registerReadTool('kube_logs', 'Read Kubernetes Pod Logs'); const helmOperations = directDomainOperations.helm.filter((operation) => internalServer.getTools().some((tool) => tool.name === operation.internalName), ); if (helmOperations.length > 0) { const helm = createHelmTool(); server.registerTool(helm, (params) => executeDomainTool(internalServer, [...helmOperations], params), ); } const execTool = internalServer .getTools() .find((candidate) => candidate.name === POD_EXEC_TOOL_NAME); if (!execTool) throw new Error(`Required tool '${POD_EXEC_TOOL_NAME}' was not registered`); server.registerTool(withSensitiveToolAnnotations(execTool), (params, context) => sensitiveToolApprovalGuard.authorizeAndExecute(POD_EXEC_TOOL_NAME, params, context, () => internalServer.executeTool(POD_EXEC_TOOL_NAME, params), ), ); if (optional.argo) { const argo = createArgoTool({ workflows: optional.argoWorkflows, cronWorkflows: optional.argoCronWorkflows, }); const operations = directDomainOperations.argo.filter((operation) => operation.publicName === 'cron_list' ? optional.argoCronWorkflows : optional.argoWorkflows, ); server.registerTool(argo, (params) => executeDomainTool(internalServer, operations, params)); } if (optional.argocd) { const argocd = createArgoCDTool(); server.registerTool(argocd, (params) => executeDomainTool(internalServer, [...directDomainOperations.argocd], params), ); } } export async function createServerForMode( mode: MCPMode, options: ConstructorParameters[0] = {}, ): Promise { const effectiveMode = mode === 'tools' ? 'tools' : 'code'; const server = new MCPServer({ ...options, toolListCacheHint: options.toolListCacheHint ?? (effectiveMode === 'tools' ? { ttlMs: 60_000, cacheScope: 'private' } : { ttlMs: 300_000, cacheScope: 'public' }), }); switch (mode) { case 'code': case 'all': await configureCodeModeServer(server); break; case 'tools': await configureToolsModeServer(server); break; default: await configureCodeModeServer(server); break; } return server; } function getReadyMessage(mode: MCPMode): string { switch (mode) { case 'code': case 'all': return 'KubeView MCP is running in code mode with `run_code` and approval-gated `kube_pod_exec`.'; case 'tools': return 'KubeView MCP is running in tools-mode. Only Kubernetes/Helm/Argo tools exposed.'; default: return 'KubeView MCP is running in code mode.'; } } export async function main(): Promise { console.error(`Kubernetes MCP Server v${VERSION} - Starting...`); try { const mode = getMCPMode(); const transportConfig = loadTransportConfig(); assertSensitiveApprovalDeploymentConfig(transportConfig.transport); if (transportConfig.transport === 'http') { const runtime = new StreamableHttpRuntime(transportConfig.http, { createAppServer: () => createServerForMode(mode, { skipGracefulShutdown: true, }), }); await runtime.start(); const { host, port, path } = runtime.getAddress(); runtime.logInfo(`${getReadyMessage(mode)} HTTP endpoint: http://${host}:${port}${path}`); return; } const stdio = serveStdio( async () => { const server = await createServerForMode(mode, { skipTransportErrorHandling: true, skipGracefulShutdown: true, }); server.logStartupBegin(); const sdkServer = server.getServer(); const previousOnClose = sdkServer.onclose; sdkServer.onclose = () => { previousOnClose?.(); void server.cleanupAfterTransportClose(); }; server.logStartupSuccess(); return sdkServer; }, { onerror: (error) => console.error('MCP stdio transport error:', error), }, ); const closeStdio = async () => { await stdio.close(); }; process.once('SIGINT', () => void closeStdio()); process.once('SIGTERM', () => void closeStdio()); console.error(getReadyMessage(mode)); } catch (error) { console.error('Failed to start MCP server:', error); process.exit(1); } }