kern0-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,216 @@
1
+ import {
2
+ Server,
3
+ } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import {
5
+ StdioServerTransport,
6
+ } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import {
8
+ ListResourcesRequestSchema,
9
+ ReadResourceRequestSchema,
10
+ ListToolsRequestSchema,
11
+ CallToolRequestSchema,
12
+ ErrorCode,
13
+ McpError,
14
+ } from "@modelcontextprotocol/sdk/types.js";
15
+ import { fetchAPI } from "./api-client.js";
16
+ import {
17
+ getOrganizationResources,
18
+ getEnvironmentResources,
19
+ getAuditLogResources,
20
+ getTokenResources,
21
+ } from "./resources.js";
22
+ import {
23
+ listTools,
24
+ executeTool,
25
+ } from "./tools.js";
26
+
27
+ const server = new Server({
28
+ name: "kernzero-mcp",
29
+ version: "1.0.0",
30
+ });
31
+
32
+ // List available resources
33
+ server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
34
+ try {
35
+ const apiUrl = process.env.KERN0_API_URL || "https://staging.kernzero.com";
36
+ const token = process.env.KERN0_TOKEN;
37
+
38
+ if (!token) {
39
+ throw new McpError(
40
+ ErrorCode.InvalidRequest,
41
+ "KERN0_TOKEN environment variable is required"
42
+ );
43
+ }
44
+
45
+ // Fetch authenticated user and accessible orgs
46
+ const me = await fetchAPI("/api/v1/auth/me", token, apiUrl);
47
+ if (!me.authenticated) {
48
+ throw new McpError(ErrorCode.InvalidRequest, "Authentication failed");
49
+ }
50
+
51
+ const userId = me.userId;
52
+ const resources = [];
53
+
54
+ // Get organizations
55
+ const orgsResponse = await fetchAPI(
56
+ "/api/v1/orgs",
57
+ token,
58
+ apiUrl
59
+ );
60
+
61
+ if (Array.isArray(orgsResponse)) {
62
+ for (const org of orgsResponse) {
63
+ resources.push(...getOrganizationResources(org));
64
+ resources.push(...getEnvironmentResources(org.id));
65
+ resources.push(...getAuditLogResources(org.id));
66
+ resources.push(...getTokenResources(org.id));
67
+ }
68
+ }
69
+
70
+ return { resources };
71
+ } catch (error) {
72
+ throw new McpError(
73
+ ErrorCode.InternalError,
74
+ `Failed to list resources: ${error}`
75
+ );
76
+ }
77
+ });
78
+
79
+ // Read resource
80
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
81
+ try {
82
+ const apiUrl = process.env.KERN0_API_URL || "https://staging.kernzero.com";
83
+ const token = process.env.KERN0_TOKEN;
84
+
85
+ if (!token) {
86
+ throw new McpError(
87
+ ErrorCode.InvalidRequest,
88
+ "KERN0_TOKEN environment variable is required"
89
+ );
90
+ }
91
+
92
+ const uri = request.params.uri;
93
+
94
+ // Parse URI format: kernzero://resource-type/id
95
+ const match = uri.match(/kernzero:\/\/([^/]+)\/(.+)/);
96
+ if (!match) {
97
+ throw new McpError(ErrorCode.InvalidRequest, `Invalid URI format: ${uri}`);
98
+ }
99
+
100
+ const [, resourceType, resourceId] = match;
101
+
102
+ let content = "";
103
+ switch (resourceType) {
104
+ case "organization": {
105
+ const org = await fetchAPI(
106
+ `/api/v1/orgs/${resourceId}`,
107
+ token,
108
+ apiUrl
109
+ );
110
+ content = JSON.stringify(org, null, 2);
111
+ break;
112
+ }
113
+
114
+ case "environments": {
115
+ const envs = await fetchAPI(
116
+ `/api/v1/orgs/${resourceId}/environments`,
117
+ token,
118
+ apiUrl
119
+ );
120
+ content = JSON.stringify(envs, null, 2);
121
+ break;
122
+ }
123
+
124
+ case "tokens": {
125
+ const tokens = await fetchAPI(
126
+ `/api/v1/orgs/${resourceId}/tokens`,
127
+ token,
128
+ apiUrl
129
+ );
130
+ content = JSON.stringify(tokens, null, 2);
131
+ break;
132
+ }
133
+
134
+ case "audit-logs": {
135
+ const logs = await fetchAPI(
136
+ `/api/v1/orgs/${resourceId}/audit-logs`,
137
+ token,
138
+ apiUrl
139
+ );
140
+ content = JSON.stringify(logs, null, 2);
141
+ break;
142
+ }
143
+
144
+ default:
145
+ throw new McpError(
146
+ ErrorCode.InvalidRequest,
147
+ `Unknown resource type: ${resourceType}`
148
+ );
149
+ }
150
+
151
+ return {
152
+ contents: [
153
+ {
154
+ uri,
155
+ mimeType: "application/json",
156
+ text: content,
157
+ },
158
+ ],
159
+ };
160
+ } catch (error) {
161
+ throw new McpError(
162
+ ErrorCode.InternalError,
163
+ `Failed to read resource: ${error}`
164
+ );
165
+ }
166
+ });
167
+
168
+ // List tools
169
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
170
+ return { tools: listTools() };
171
+ });
172
+
173
+ // Call tool
174
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
175
+ try {
176
+ const apiUrl = process.env.KERN0_API_URL || "https://staging.kernzero.com";
177
+ const token = process.env.KERN0_TOKEN;
178
+
179
+ if (!token) {
180
+ throw new McpError(
181
+ ErrorCode.InvalidRequest,
182
+ "KERN0_TOKEN environment variable is required"
183
+ );
184
+ }
185
+
186
+ const result = await executeTool(
187
+ request.params.name,
188
+ request.params.arguments || {},
189
+ token,
190
+ apiUrl
191
+ );
192
+
193
+ return {
194
+ content: [
195
+ {
196
+ type: "text",
197
+ text: JSON.stringify(result, null, 2),
198
+ },
199
+ ],
200
+ };
201
+ } catch (error) {
202
+ throw new McpError(
203
+ ErrorCode.InternalError,
204
+ `Tool execution failed: ${error}`
205
+ );
206
+ }
207
+ });
208
+
209
+ // Start server
210
+ async function main() {
211
+ const transport = new StdioServerTransport();
212
+ await server.connect(transport);
213
+ console.error("KernZero MCP server started");
214
+ }
215
+
216
+ main().catch(console.error);
@@ -0,0 +1,288 @@
1
+ /**
2
+ * MCP tool definitions for KernZero
3
+ * Tools represent actions that Claude can execute
4
+ */
5
+
6
+ import { Tool } from "@modelcontextprotocol/sdk/types.js";
7
+ import { fetchAPI, callAPI } from "./api-client.js";
8
+
9
+ export function listTools(): Tool[] {
10
+ return [
11
+ {
12
+ name: "get_user_context",
13
+ description: "Get current authenticated user and organization context",
14
+ inputSchema: {
15
+ type: "object",
16
+ properties: {},
17
+ required: [],
18
+ },
19
+ },
20
+
21
+ {
22
+ name: "list_organizations",
23
+ description: "List all organizations the user has access to",
24
+ inputSchema: {
25
+ type: "object",
26
+ properties: {},
27
+ required: [],
28
+ },
29
+ },
30
+
31
+ {
32
+ name: "get_organization_details",
33
+ description: "Get detailed information about a specific organization",
34
+ inputSchema: {
35
+ type: "object",
36
+ properties: {
37
+ orgId: {
38
+ type: "string",
39
+ description: "Organization ID",
40
+ },
41
+ },
42
+ required: ["orgId"],
43
+ },
44
+ },
45
+
46
+ {
47
+ name: "list_environments",
48
+ description:
49
+ "List all monitored environments in an organization with their status",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: {
53
+ orgId: {
54
+ type: "string",
55
+ description: "Organization ID",
56
+ },
57
+ },
58
+ required: ["orgId"],
59
+ },
60
+ },
61
+
62
+ {
63
+ name: "get_environment_status",
64
+ description: "Get detailed status and health metrics for an environment",
65
+ inputSchema: {
66
+ type: "object",
67
+ properties: {
68
+ orgId: {
69
+ type: "string",
70
+ description: "Organization ID",
71
+ },
72
+ envId: {
73
+ type: "string",
74
+ description: "Environment ID",
75
+ },
76
+ },
77
+ required: ["orgId", "envId"],
78
+ },
79
+ },
80
+
81
+ {
82
+ name: "list_audit_logs",
83
+ description:
84
+ "Get audit trail showing who performed what actions and when",
85
+ inputSchema: {
86
+ type: "object",
87
+ properties: {
88
+ orgId: {
89
+ type: "string",
90
+ description: "Organization ID",
91
+ },
92
+ limit: {
93
+ type: "number",
94
+ description: "Number of logs to retrieve (default: 50)",
95
+ },
96
+ },
97
+ required: ["orgId"],
98
+ },
99
+ },
100
+
101
+ {
102
+ name: "list_tokens",
103
+ description: "List Personal Access Tokens and their scopes",
104
+ inputSchema: {
105
+ type: "object",
106
+ properties: {
107
+ orgId: {
108
+ type: "string",
109
+ description: "Organization ID",
110
+ },
111
+ },
112
+ required: ["orgId"],
113
+ },
114
+ },
115
+
116
+ {
117
+ name: "analyze_readiness",
118
+ description:
119
+ "Analyze recovery readiness for an organization based on environment status",
120
+ inputSchema: {
121
+ type: "object",
122
+ properties: {
123
+ orgId: {
124
+ type: "string",
125
+ description: "Organization ID to analyze",
126
+ },
127
+ },
128
+ required: ["orgId"],
129
+ },
130
+ },
131
+
132
+ {
133
+ name: "check_backup_health",
134
+ description: "Check backup health status for all environments",
135
+ inputSchema: {
136
+ type: "object",
137
+ properties: {
138
+ orgId: {
139
+ type: "string",
140
+ description: "Organization ID",
141
+ },
142
+ },
143
+ required: ["orgId"],
144
+ },
145
+ },
146
+ ];
147
+ }
148
+
149
+ export async function executeTool(
150
+ name: string,
151
+ args: Record<string, any>,
152
+ token: string,
153
+ apiUrl: string
154
+ ): Promise<any> {
155
+ switch (name) {
156
+ case "get_user_context": {
157
+ return await fetchAPI("/api/v1/auth/me", token, apiUrl);
158
+ }
159
+
160
+ case "list_organizations": {
161
+ const orgs = await fetchAPI("/api/v1/orgs", token, apiUrl);
162
+ return Array.isArray(orgs) ? orgs : orgs.organizations || [];
163
+ }
164
+
165
+ case "get_organization_details": {
166
+ if (!args.orgId) throw new Error("orgId is required");
167
+ return await fetchAPI(`/api/v1/orgs/${args.orgId}`, token, apiUrl);
168
+ }
169
+
170
+ case "list_environments": {
171
+ if (!args.orgId) throw new Error("orgId is required");
172
+ return await fetchAPI(
173
+ `/api/v1/orgs/${args.orgId}/environments`,
174
+ token,
175
+ apiUrl
176
+ );
177
+ }
178
+
179
+ case "get_environment_status": {
180
+ if (!args.orgId || !args.envId)
181
+ throw new Error("orgId and envId are required");
182
+ const envs = await fetchAPI(
183
+ `/api/v1/orgs/${args.orgId}/environments`,
184
+ token,
185
+ apiUrl
186
+ );
187
+ const env = (envs.environments || []).find(
188
+ (e: any) => e.id === args.envId
189
+ );
190
+ return env || { error: "Environment not found" };
191
+ }
192
+
193
+ case "list_audit_logs": {
194
+ if (!args.orgId) throw new Error("orgId is required");
195
+ return await fetchAPI(
196
+ `/api/v1/orgs/${args.orgId}/audit-logs?limit=${args.limit || 50}`,
197
+ token,
198
+ apiUrl
199
+ );
200
+ }
201
+
202
+ case "list_tokens": {
203
+ if (!args.orgId) throw new Error("orgId is required");
204
+ return await fetchAPI(`/api/v1/orgs/${args.orgId}/tokens`, token, apiUrl);
205
+ }
206
+
207
+ case "analyze_readiness": {
208
+ if (!args.orgId) throw new Error("orgId is required");
209
+ const envs = await fetchAPI(
210
+ `/api/v1/orgs/${args.orgId}/environments`,
211
+ token,
212
+ apiUrl
213
+ );
214
+
215
+ const environments = envs.environments || [];
216
+ const onlineCount = environments.filter(
217
+ (e: any) => e.status === "online"
218
+ ).length;
219
+ const offlineCount = environments.filter(
220
+ (e: any) => e.status === "offline"
221
+ ).length;
222
+ const validBackups = environments.filter(
223
+ (e: any) => e.hasValidBackup || e.hasValidBackupByPeer
224
+ ).length;
225
+
226
+ const readiness = environments.length > 0
227
+ ? Math.round(
228
+ ((validBackups / environments.length) * 100 +
229
+ (onlineCount / Math.max(1, environments.length)) * 50) / 1.5
230
+ )
231
+ : 0;
232
+
233
+ return {
234
+ totalEnvironments: environments.length,
235
+ onlineCount,
236
+ offlineCount,
237
+ validBackupsCount: validBackups,
238
+ readinessPercentage: readiness,
239
+ assessment:
240
+ readiness >= 80
241
+ ? "READY - High recovery readiness"
242
+ : readiness >= 60
243
+ ? "PARTIAL - Some gaps to address"
244
+ : readiness >= 40
245
+ ? "AT RISK - Multiple issues"
246
+ : "CRITICAL - Immediate attention needed",
247
+ };
248
+ }
249
+
250
+ case "check_backup_health": {
251
+ if (!args.orgId) throw new Error("orgId is required");
252
+ const envs = await fetchAPI(
253
+ `/api/v1/orgs/${args.orgId}/environments`,
254
+ token,
255
+ apiUrl
256
+ );
257
+
258
+ const environments = envs.environments || [];
259
+ const backupStatus = environments.map((env: any) => ({
260
+ environment: env.label,
261
+ status: env.status,
262
+ hasValidBackup: env.hasValidBackup,
263
+ hasValidBackupByPeer: env.hasValidBackupByPeer,
264
+ lastBackupTime: env.lastBackupTime,
265
+ backupHealth:
266
+ env.hasValidBackup || env.hasValidBackupByPeer ? "✅ HEALTHY" : "⚠️ NEEDS BACKUP",
267
+ }));
268
+
269
+ const unhealthy = backupStatus.filter(
270
+ (b: any) => b.backupHealth !== "✅ HEALTHY"
271
+ );
272
+
273
+ return {
274
+ totalEnvironments: environments.length,
275
+ healthyBackups: backupStatus.length - unhealthy.length,
276
+ unhealthyBackups: unhealthy.length,
277
+ details: backupStatus,
278
+ summary:
279
+ unhealthy.length === 0
280
+ ? "All backups are healthy"
281
+ : `${unhealthy.length} environment(s) need backup attention`,
282
+ };
283
+ }
284
+
285
+ default:
286
+ throw new Error(`Unknown tool: ${name}`);
287
+ }
288
+ }
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "kern0-cli",
3
+ "version": "0.1.0",
4
+ "description": "KernZero CLI - Recovery control plane management from the terminal",
5
+ "private": false,
6
+ "license": "MIT",
7
+ "author": "KernZero",
8
+ "homepage": "https://kernzero.com",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/santorres/kern0.git"
12
+ },
13
+ "keywords": [
14
+ "recovery",
15
+ "cli",
16
+ "backup",
17
+ "disaster-recovery",
18
+ "active-directory",
19
+ "zero-trust",
20
+ "passkeys",
21
+ "device-flow"
22
+ ],
23
+ "bin": {
24
+ "kern0": "./bin/kern0",
25
+ "kern0-mcp": "./bin/kern0-mcp"
26
+ },
27
+ "files": [
28
+ "lib/cli",
29
+ "lib/mcp",
30
+ "bin",
31
+ "package.json"
32
+ ],
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ },
36
+ "type": "module",
37
+ "scripts": {
38
+ "dev": "next dev",
39
+ "build": "next build",
40
+ "start": "next start",
41
+ "lint": "eslint",
42
+ "typecheck": "tsc --noEmit",
43
+ "firebase:init": "node scripts/bootstrap.mjs",
44
+ "firebase:probe": "node scripts/probe.mjs",
45
+ "seed:v1": "node scripts/seed-v1-samples.mjs",
46
+ "patch:iroh": "node scripts/patch-iroh.mjs",
47
+ "postinstall": "node scripts/patch-iroh.mjs"
48
+ },
49
+ "dependencies": {
50
+ "@aws-sdk/client-s3": "^3.1127.0",
51
+ "@modelcontextprotocol/sdk": "^1.30.1",
52
+ "@number0/iroh": "^1.1.0",
53
+ "@simplewebauthn/browser": "^14.0.0",
54
+ "@simplewebauthn/server": "^14.0.3",
55
+ "@types/qrcode": "^1.5.6",
56
+ "firebase": "^12.18.0",
57
+ "firebase-admin": "^14.5.0",
58
+ "lucide-react": "^1.41.0",
59
+ "next": "16.3.4",
60
+ "open": "^10.0.0",
61
+ "qrcode": "^1.5.4",
62
+ "react": "19.2.8",
63
+ "react-dom": "19.2.8"
64
+ },
65
+ "devDependencies": {
66
+ "@tailwindcss/postcss": "^4",
67
+ "@types/node": "^20",
68
+ "@types/react": "^19",
69
+ "@types/react-dom": "^19",
70
+ "eslint": "^9",
71
+ "eslint-config-next": "16.3.4",
72
+ "tailwindcss": "^4",
73
+ "tsx": "^4.23.15",
74
+ "typescript": "^5"
75
+ }
76
+ }