@colmbus72/yeehaw 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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/dist/app.d.ts +1 -0
  4. package/dist/app.js +414 -0
  5. package/dist/components/BarnHeader.d.ts +6 -0
  6. package/dist/components/BarnHeader.js +21 -0
  7. package/dist/components/BottomBar.d.ts +16 -0
  8. package/dist/components/BottomBar.js +7 -0
  9. package/dist/components/Header.d.ts +8 -0
  10. package/dist/components/Header.js +83 -0
  11. package/dist/components/HelpOverlay.d.ts +7 -0
  12. package/dist/components/HelpOverlay.js +17 -0
  13. package/dist/components/List.d.ts +17 -0
  14. package/dist/components/List.js +53 -0
  15. package/dist/components/Markdown.d.ts +8 -0
  16. package/dist/components/Markdown.js +23 -0
  17. package/dist/components/Panel.d.ts +10 -0
  18. package/dist/components/Panel.js +5 -0
  19. package/dist/components/PathInput.d.ts +9 -0
  20. package/dist/components/PathInput.js +141 -0
  21. package/dist/components/ScrollableMarkdown.d.ts +11 -0
  22. package/dist/components/ScrollableMarkdown.js +56 -0
  23. package/dist/components/StatusBar.d.ts +5 -0
  24. package/dist/components/StatusBar.js +20 -0
  25. package/dist/components/TextArea.d.ts +17 -0
  26. package/dist/components/TextArea.js +140 -0
  27. package/dist/components/index.d.ts +5 -0
  28. package/dist/components/index.js +5 -0
  29. package/dist/hooks/index.d.ts +3 -0
  30. package/dist/hooks/index.js +3 -0
  31. package/dist/hooks/useConfig.d.ts +11 -0
  32. package/dist/hooks/useConfig.js +36 -0
  33. package/dist/hooks/useRemoteYeehaw.d.ts +13 -0
  34. package/dist/hooks/useRemoteYeehaw.js +49 -0
  35. package/dist/hooks/useSessions.d.ts +11 -0
  36. package/dist/hooks/useSessions.js +46 -0
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +34 -0
  39. package/dist/lib/config.d.ts +27 -0
  40. package/dist/lib/config.js +150 -0
  41. package/dist/lib/detection.d.ts +16 -0
  42. package/dist/lib/detection.js +41 -0
  43. package/dist/lib/editor.d.ts +5 -0
  44. package/dist/lib/editor.js +35 -0
  45. package/dist/lib/errors.d.ts +28 -0
  46. package/dist/lib/errors.js +48 -0
  47. package/dist/lib/git.d.ts +11 -0
  48. package/dist/lib/git.js +73 -0
  49. package/dist/lib/github.d.ts +43 -0
  50. package/dist/lib/github.js +111 -0
  51. package/dist/lib/hotkeys.d.ts +27 -0
  52. package/dist/lib/hotkeys.js +92 -0
  53. package/dist/lib/index.d.ts +10 -0
  54. package/dist/lib/index.js +10 -0
  55. package/dist/lib/livestock.d.ts +51 -0
  56. package/dist/lib/livestock.js +233 -0
  57. package/dist/lib/mcp-validation.d.ts +33 -0
  58. package/dist/lib/mcp-validation.js +62 -0
  59. package/dist/lib/paths.d.ts +8 -0
  60. package/dist/lib/paths.js +28 -0
  61. package/dist/lib/shell.d.ts +34 -0
  62. package/dist/lib/shell.js +61 -0
  63. package/dist/lib/ssh.d.ts +15 -0
  64. package/dist/lib/ssh.js +77 -0
  65. package/dist/lib/tmux-config.d.ts +3 -0
  66. package/dist/lib/tmux-config.js +42 -0
  67. package/dist/lib/tmux.d.ts +32 -0
  68. package/dist/lib/tmux.js +397 -0
  69. package/dist/mcp-server.d.ts +23 -0
  70. package/dist/mcp-server.js +825 -0
  71. package/dist/types.d.ts +89 -0
  72. package/dist/types.js +2 -0
  73. package/dist/views/BarnContext.d.ts +22 -0
  74. package/dist/views/BarnContext.js +252 -0
  75. package/dist/views/GlobalDashboard.d.ts +16 -0
  76. package/dist/views/GlobalDashboard.js +253 -0
  77. package/dist/views/Home.d.ts +11 -0
  78. package/dist/views/Home.js +27 -0
  79. package/dist/views/IssuesView.d.ts +7 -0
  80. package/dist/views/IssuesView.js +157 -0
  81. package/dist/views/LivestockDetailView.d.ts +11 -0
  82. package/dist/views/LivestockDetailView.js +140 -0
  83. package/dist/views/LogsView.d.ts +8 -0
  84. package/dist/views/LogsView.js +84 -0
  85. package/dist/views/NightSkyView.d.ts +5 -0
  86. package/dist/views/NightSkyView.js +441 -0
  87. package/dist/views/ProjectContext.d.ts +18 -0
  88. package/dist/views/ProjectContext.js +333 -0
  89. package/dist/views/Projects.d.ts +8 -0
  90. package/dist/views/Projects.js +20 -0
  91. package/dist/views/WikiView.d.ts +8 -0
  92. package/dist/views/WikiView.js +138 -0
  93. package/dist/views/index.d.ts +2 -0
  94. package/dist/views/index.js +2 -0
  95. package/package.json +65 -0
@@ -0,0 +1,825 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Yeehaw MCP Server
4
+ *
5
+ * Exposes Yeehaw project/barn/livestock management to Claude Code sessions.
6
+ *
7
+ * Terminology:
8
+ * - Project: A codebase you're working on
9
+ * - Barn: A server you manage
10
+ * - Livestock: Deployed instances of your apps (local or on barns)
11
+ * - Critter: System services that support livestock (nginx, mysql, etc.)
12
+ *
13
+ * Usage: Add to Claude Code's MCP config:
14
+ * {
15
+ * "mcpServers": {
16
+ * "yeehaw": {
17
+ * "command": "node",
18
+ * "args": ["/path/to/yeehaw/dist/mcp-server.js"]
19
+ * }
20
+ * }
21
+ * }
22
+ */
23
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
24
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
25
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
26
+ import { loadProjects, loadProject, loadBarns, loadBarn, saveProject, saveBarn, deleteProject, deleteBarn, getLivestockForBarn, ensureConfigDirs, } from './lib/config.js';
27
+ import { readLivestockLogs, readLivestockEnv } from './lib/livestock.js';
28
+ import { requireString, optionalString, optionalNumber, optionalBoolean, } from './lib/mcp-validation.js';
29
+ const server = new Server({
30
+ name: 'yeehaw',
31
+ version: '0.2.0',
32
+ }, {
33
+ capabilities: {
34
+ tools: {},
35
+ resources: {},
36
+ },
37
+ });
38
+ // ============================================================================
39
+ // Resources - Data Claude can read
40
+ // ============================================================================
41
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
42
+ const projects = loadProjects();
43
+ const barns = loadBarns();
44
+ return {
45
+ resources: [
46
+ ...projects.map((p) => ({
47
+ uri: `yeehaw://project/${p.name}`,
48
+ name: `Project: ${p.name}`,
49
+ description: p.summary || `Project at ${p.path}`,
50
+ mimeType: 'application/json',
51
+ })),
52
+ ...barns.map((b) => ({
53
+ uri: `yeehaw://barn/${b.name}`,
54
+ name: `Barn: ${b.name}`,
55
+ description: b.name === 'local' ? 'Local machine' : `Server at ${b.host}`,
56
+ mimeType: 'application/json',
57
+ })),
58
+ {
59
+ uri: 'yeehaw://projects',
60
+ name: 'All Projects',
61
+ description: 'List of all Yeehaw projects',
62
+ mimeType: 'application/json',
63
+ },
64
+ {
65
+ uri: 'yeehaw://barns',
66
+ name: 'All Barns',
67
+ description: 'List of all Yeehaw barns (servers)',
68
+ mimeType: 'application/json',
69
+ },
70
+ ],
71
+ };
72
+ });
73
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
74
+ const { uri } = request.params;
75
+ if (uri === 'yeehaw://projects') {
76
+ const projects = loadProjects();
77
+ return {
78
+ contents: [
79
+ {
80
+ uri,
81
+ mimeType: 'application/json',
82
+ text: JSON.stringify(projects, null, 2),
83
+ },
84
+ ],
85
+ };
86
+ }
87
+ if (uri === 'yeehaw://barns') {
88
+ const barns = loadBarns();
89
+ return {
90
+ contents: [
91
+ {
92
+ uri,
93
+ mimeType: 'application/json',
94
+ text: JSON.stringify(barns, null, 2),
95
+ },
96
+ ],
97
+ };
98
+ }
99
+ const projectMatch = uri.match(/^yeehaw:\/\/project\/(.+)$/);
100
+ if (projectMatch) {
101
+ const project = loadProject(projectMatch[1]);
102
+ if (!project) {
103
+ throw new Error(`Project not found: ${projectMatch[1]}`);
104
+ }
105
+ return {
106
+ contents: [
107
+ {
108
+ uri,
109
+ mimeType: 'application/json',
110
+ text: JSON.stringify(project, null, 2),
111
+ },
112
+ ],
113
+ };
114
+ }
115
+ const barnMatch = uri.match(/^yeehaw:\/\/barn\/(.+)$/);
116
+ if (barnMatch) {
117
+ const barn = loadBarn(barnMatch[1]);
118
+ if (!barn) {
119
+ throw new Error(`Barn not found: ${barnMatch[1]}`);
120
+ }
121
+ // Include livestock deployed to this barn
122
+ const livestock = getLivestockForBarn(barn.name);
123
+ return {
124
+ contents: [
125
+ {
126
+ uri,
127
+ mimeType: 'application/json',
128
+ text: JSON.stringify({ ...barn, deployedLivestock: livestock }, null, 2),
129
+ },
130
+ ],
131
+ };
132
+ }
133
+ throw new Error(`Unknown resource: ${uri}`);
134
+ });
135
+ // ============================================================================
136
+ // Tools - Actions Claude can take
137
+ // ============================================================================
138
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
139
+ return {
140
+ tools: [
141
+ // Project tools
142
+ {
143
+ name: 'list_projects',
144
+ description: 'List all Yeehaw projects',
145
+ inputSchema: {
146
+ type: 'object',
147
+ properties: {},
148
+ },
149
+ },
150
+ {
151
+ name: 'get_project',
152
+ description: 'Get details of a specific project including its livestock',
153
+ inputSchema: {
154
+ type: 'object',
155
+ properties: {
156
+ name: { type: 'string', description: 'Project name' },
157
+ },
158
+ required: ['name'],
159
+ },
160
+ },
161
+ {
162
+ name: 'create_project',
163
+ description: 'Create a new project',
164
+ inputSchema: {
165
+ type: 'object',
166
+ properties: {
167
+ name: { type: 'string', description: 'Project name' },
168
+ path: { type: 'string', description: 'Local path to project' },
169
+ summary: { type: 'string', description: 'Short description' },
170
+ color: { type: 'string', description: 'Hex color (e.g., #ff6b6b)' },
171
+ },
172
+ required: ['name', 'path'],
173
+ },
174
+ },
175
+ {
176
+ name: 'update_project',
177
+ description: 'Update an existing project',
178
+ inputSchema: {
179
+ type: 'object',
180
+ properties: {
181
+ name: { type: 'string', description: 'Project name to update' },
182
+ summary: { type: 'string', description: 'New summary' },
183
+ color: { type: 'string', description: 'New hex color' },
184
+ path: { type: 'string', description: 'New path' },
185
+ },
186
+ required: ['name'],
187
+ },
188
+ },
189
+ {
190
+ name: 'delete_project',
191
+ description: 'Delete a project (requires confirmation)',
192
+ inputSchema: {
193
+ type: 'object',
194
+ properties: {
195
+ name: { type: 'string', description: 'Project name to delete' },
196
+ confirm: { type: 'string', description: 'Must match project name to confirm deletion' },
197
+ },
198
+ required: ['name', 'confirm'],
199
+ },
200
+ },
201
+ // Livestock tools
202
+ {
203
+ name: 'add_livestock',
204
+ description: 'Add livestock (deployed app instance) to a project',
205
+ inputSchema: {
206
+ type: 'object',
207
+ properties: {
208
+ project: { type: 'string', description: 'Project name' },
209
+ name: { type: 'string', description: 'Livestock name (e.g., local, dev, production)' },
210
+ path: { type: 'string', description: 'Path (local or remote)' },
211
+ barn: { type: 'string', description: 'Barn name for remote livestock' },
212
+ repo: { type: 'string', description: 'Git repository URL' },
213
+ branch: { type: 'string', description: 'Git branch' },
214
+ log_path: { type: 'string', description: 'Path to logs relative to livestock path (e.g., storage/logs/)' },
215
+ env_path: { type: 'string', description: 'Path to env file relative to livestock path (e.g., .env)' },
216
+ },
217
+ required: ['project', 'name', 'path'],
218
+ },
219
+ },
220
+ {
221
+ name: 'remove_livestock',
222
+ description: 'Remove livestock from a project',
223
+ inputSchema: {
224
+ type: 'object',
225
+ properties: {
226
+ project: { type: 'string', description: 'Project name' },
227
+ name: { type: 'string', description: 'Livestock name to remove' },
228
+ },
229
+ required: ['project', 'name'],
230
+ },
231
+ },
232
+ {
233
+ name: 'read_livestock_logs',
234
+ description: 'Read log files from a livestock deployment',
235
+ inputSchema: {
236
+ type: 'object',
237
+ properties: {
238
+ project: { type: 'string', description: 'Project name' },
239
+ livestock: { type: 'string', description: 'Livestock name (e.g., production)' },
240
+ lines: { type: 'number', description: 'Last N lines (default: 100)' },
241
+ pattern: { type: 'string', description: 'Grep pattern to filter logs (case-insensitive)' },
242
+ },
243
+ required: ['project', 'livestock'],
244
+ },
245
+ },
246
+ {
247
+ name: 'read_livestock_env',
248
+ description: 'Read environment config from a livestock deployment',
249
+ inputSchema: {
250
+ type: 'object',
251
+ properties: {
252
+ project: { type: 'string', description: 'Project name' },
253
+ livestock: { type: 'string', description: 'Livestock name' },
254
+ show_values: { type: 'boolean', description: 'Show values (default: false, keys only for security)' },
255
+ },
256
+ required: ['project', 'livestock'],
257
+ },
258
+ },
259
+ // Barn tools
260
+ {
261
+ name: 'list_barns',
262
+ description: 'List all Yeehaw barns (servers)',
263
+ inputSchema: {
264
+ type: 'object',
265
+ properties: {},
266
+ },
267
+ },
268
+ {
269
+ name: 'get_barn',
270
+ description: 'Get details of a specific barn including deployed livestock',
271
+ inputSchema: {
272
+ type: 'object',
273
+ properties: {
274
+ name: { type: 'string', description: 'Barn name' },
275
+ },
276
+ required: ['name'],
277
+ },
278
+ },
279
+ {
280
+ name: 'create_barn',
281
+ description: 'Create a new barn (server)',
282
+ inputSchema: {
283
+ type: 'object',
284
+ properties: {
285
+ name: { type: 'string', description: 'Barn name (identifier)' },
286
+ host: { type: 'string', description: 'Hostname or IP address' },
287
+ user: { type: 'string', description: 'SSH username' },
288
+ port: { type: 'number', description: 'SSH port (default: 22)' },
289
+ identity_file: { type: 'string', description: 'Path to SSH private key' },
290
+ },
291
+ required: ['name', 'host', 'user', 'identity_file'],
292
+ },
293
+ },
294
+ {
295
+ name: 'update_barn',
296
+ description: 'Update an existing barn',
297
+ inputSchema: {
298
+ type: 'object',
299
+ properties: {
300
+ name: { type: 'string', description: 'Barn name to update' },
301
+ host: { type: 'string', description: 'New hostname or IP' },
302
+ user: { type: 'string', description: 'New SSH username' },
303
+ port: { type: 'number', description: 'New SSH port' },
304
+ identity_file: { type: 'string', description: 'New path to SSH key' },
305
+ },
306
+ required: ['name'],
307
+ },
308
+ },
309
+ {
310
+ name: 'delete_barn',
311
+ description: 'Delete a barn (requires confirmation)',
312
+ inputSchema: {
313
+ type: 'object',
314
+ properties: {
315
+ name: { type: 'string', description: 'Barn name to delete' },
316
+ confirm: { type: 'string', description: 'Must match barn name to confirm deletion' },
317
+ },
318
+ required: ['name', 'confirm'],
319
+ },
320
+ },
321
+ // Wiki tools
322
+ {
323
+ name: 'get_wiki',
324
+ description: 'Get all wiki sections for a project',
325
+ inputSchema: {
326
+ type: 'object',
327
+ properties: {
328
+ project: { type: 'string', description: 'Project name' },
329
+ },
330
+ required: ['project'],
331
+ },
332
+ },
333
+ {
334
+ name: 'add_wiki_section',
335
+ description: 'Add a new wiki section to a project',
336
+ inputSchema: {
337
+ type: 'object',
338
+ properties: {
339
+ project: { type: 'string', description: 'Project name' },
340
+ title: { type: 'string', description: 'Section title' },
341
+ content: { type: 'string', description: 'Section content (markdown)' },
342
+ },
343
+ required: ['project', 'title', 'content'],
344
+ },
345
+ },
346
+ {
347
+ name: 'update_wiki_section',
348
+ description: 'Update an existing wiki section',
349
+ inputSchema: {
350
+ type: 'object',
351
+ properties: {
352
+ project: { type: 'string', description: 'Project name' },
353
+ title: { type: 'string', description: 'Section title to update' },
354
+ new_title: { type: 'string', description: 'New title (optional)' },
355
+ content: { type: 'string', description: 'New content (optional)' },
356
+ },
357
+ required: ['project', 'title'],
358
+ },
359
+ },
360
+ {
361
+ name: 'delete_wiki_section',
362
+ description: 'Delete a wiki section from a project',
363
+ inputSchema: {
364
+ type: 'object',
365
+ properties: {
366
+ project: { type: 'string', description: 'Project name' },
367
+ title: { type: 'string', description: 'Section title to delete' },
368
+ },
369
+ required: ['project', 'title'],
370
+ },
371
+ },
372
+ ],
373
+ };
374
+ });
375
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
376
+ const { name, arguments: args } = request.params;
377
+ try {
378
+ switch (name) {
379
+ // Project operations
380
+ case 'list_projects': {
381
+ const projects = loadProjects();
382
+ return {
383
+ content: [
384
+ {
385
+ type: 'text',
386
+ text: JSON.stringify(projects, null, 2),
387
+ },
388
+ ],
389
+ };
390
+ }
391
+ case 'get_project': {
392
+ const name = requireString(args, 'name');
393
+ const project = loadProject(name);
394
+ if (!project) {
395
+ return {
396
+ content: [{ type: 'text', text: `Project not found: ${name}` }],
397
+ isError: true,
398
+ };
399
+ }
400
+ return {
401
+ content: [{ type: 'text', text: JSON.stringify(project, null, 2) }],
402
+ };
403
+ }
404
+ case 'create_project': {
405
+ const name = requireString(args, 'name');
406
+ const path = requireString(args, 'path');
407
+ const summary = optionalString(args, 'summary');
408
+ const color = optionalString(args, 'color');
409
+ const existing = loadProject(name);
410
+ if (existing) {
411
+ return {
412
+ content: [{ type: 'text', text: `Project already exists: ${name}` }],
413
+ isError: true,
414
+ };
415
+ }
416
+ const project = {
417
+ name,
418
+ path,
419
+ summary,
420
+ color,
421
+ livestock: [],
422
+ };
423
+ saveProject(project);
424
+ return {
425
+ content: [{ type: 'text', text: `Created project: ${project.name}` }],
426
+ };
427
+ }
428
+ case 'update_project': {
429
+ const name = requireString(args, 'name');
430
+ const summary = optionalString(args, 'summary');
431
+ const color = optionalString(args, 'color');
432
+ const path = optionalString(args, 'path');
433
+ const project = loadProject(name);
434
+ if (!project) {
435
+ return {
436
+ content: [{ type: 'text', text: `Project not found: ${name}` }],
437
+ isError: true,
438
+ };
439
+ }
440
+ if (summary !== undefined)
441
+ project.summary = summary;
442
+ if (color !== undefined)
443
+ project.color = color;
444
+ if (path !== undefined)
445
+ project.path = path;
446
+ saveProject(project);
447
+ return {
448
+ content: [{ type: 'text', text: `Updated project: ${project.name}` }],
449
+ };
450
+ }
451
+ case 'delete_project': {
452
+ const projectName = requireString(args, 'name');
453
+ const confirm = requireString(args, 'confirm');
454
+ if (confirm !== projectName) {
455
+ return {
456
+ content: [{ type: 'text', text: `Confirmation does not match. To delete, confirm must equal "${projectName}"` }],
457
+ isError: true,
458
+ };
459
+ }
460
+ const deleted = deleteProject(projectName);
461
+ if (!deleted) {
462
+ return {
463
+ content: [{ type: 'text', text: `Project not found: ${projectName}` }],
464
+ isError: true,
465
+ };
466
+ }
467
+ return {
468
+ content: [{ type: 'text', text: `Deleted project: ${projectName}` }],
469
+ };
470
+ }
471
+ // Livestock operations
472
+ case 'add_livestock': {
473
+ const projectName = requireString(args, 'project');
474
+ const livestockName = requireString(args, 'name');
475
+ const livestockPath = requireString(args, 'path');
476
+ const barn = optionalString(args, 'barn');
477
+ const repo = optionalString(args, 'repo');
478
+ const branch = optionalString(args, 'branch');
479
+ const log_path = optionalString(args, 'log_path');
480
+ const env_path = optionalString(args, 'env_path');
481
+ const project = loadProject(projectName);
482
+ if (!project) {
483
+ return {
484
+ content: [{ type: 'text', text: `Project not found: ${projectName}` }],
485
+ isError: true,
486
+ };
487
+ }
488
+ const livestock = {
489
+ name: livestockName,
490
+ path: livestockPath,
491
+ barn,
492
+ repo,
493
+ branch,
494
+ log_path,
495
+ env_path,
496
+ };
497
+ project.livestock = project.livestock || [];
498
+ // Remove existing livestock with same name
499
+ project.livestock = project.livestock.filter((l) => l.name !== livestock.name);
500
+ project.livestock.push(livestock);
501
+ saveProject(project);
502
+ return {
503
+ content: [{ type: 'text', text: `Added livestock '${livestock.name}' to project '${project.name}'` }],
504
+ };
505
+ }
506
+ case 'remove_livestock': {
507
+ const projectName = requireString(args, 'project');
508
+ const livestockName = requireString(args, 'name');
509
+ const project = loadProject(projectName);
510
+ if (!project) {
511
+ return {
512
+ content: [{ type: 'text', text: `Project not found: ${projectName}` }],
513
+ isError: true,
514
+ };
515
+ }
516
+ project.livestock = (project.livestock || []).filter((l) => l.name !== livestockName);
517
+ saveProject(project);
518
+ return {
519
+ content: [{ type: 'text', text: `Removed livestock '${livestockName}' from project '${project.name}'` }],
520
+ };
521
+ }
522
+ case 'read_livestock_logs': {
523
+ const projectName = requireString(args, 'project');
524
+ const livestockName = requireString(args, 'livestock');
525
+ const lines = optionalNumber(args, 'lines');
526
+ const pattern = optionalString(args, 'pattern');
527
+ const project = loadProject(projectName);
528
+ if (!project) {
529
+ return {
530
+ content: [{ type: 'text', text: `Project not found: ${projectName}` }],
531
+ isError: true,
532
+ };
533
+ }
534
+ const livestock = (project.livestock || []).find(l => l.name === livestockName);
535
+ if (!livestock) {
536
+ return {
537
+ content: [{ type: 'text', text: `Livestock not found: ${livestockName}` }],
538
+ isError: true,
539
+ };
540
+ }
541
+ const result = await readLivestockLogs(livestock, { lines, pattern });
542
+ if (result.error) {
543
+ return {
544
+ content: [{ type: 'text', text: result.error }],
545
+ isError: true,
546
+ };
547
+ }
548
+ return {
549
+ content: [{ type: 'text', text: result.content }],
550
+ };
551
+ }
552
+ case 'read_livestock_env': {
553
+ const projectName = requireString(args, 'project');
554
+ const livestockName = requireString(args, 'livestock');
555
+ const showValues = optionalBoolean(args, 'show_values', false);
556
+ const project = loadProject(projectName);
557
+ if (!project) {
558
+ return {
559
+ content: [{ type: 'text', text: `Project not found: ${projectName}` }],
560
+ isError: true,
561
+ };
562
+ }
563
+ const livestock = (project.livestock || []).find(l => l.name === livestockName);
564
+ if (!livestock) {
565
+ return {
566
+ content: [{ type: 'text', text: `Livestock not found: ${livestockName}` }],
567
+ isError: true,
568
+ };
569
+ }
570
+ const result = await readLivestockEnv(livestock, showValues);
571
+ if (result.error) {
572
+ return {
573
+ content: [{ type: 'text', text: result.error }],
574
+ isError: true,
575
+ };
576
+ }
577
+ return {
578
+ content: [{ type: 'text', text: result.content }],
579
+ };
580
+ }
581
+ // Barn operations
582
+ case 'list_barns': {
583
+ const barns = loadBarns();
584
+ return {
585
+ content: [{ type: 'text', text: JSON.stringify(barns, null, 2) }],
586
+ };
587
+ }
588
+ case 'get_barn': {
589
+ const barnName = requireString(args, 'name');
590
+ const barn = loadBarn(barnName);
591
+ if (!barn) {
592
+ return {
593
+ content: [{ type: 'text', text: `Barn not found: ${barnName}` }],
594
+ isError: true,
595
+ };
596
+ }
597
+ // Include livestock deployed to this barn
598
+ const livestock = getLivestockForBarn(barn.name);
599
+ return {
600
+ content: [{ type: 'text', text: JSON.stringify({ ...barn, deployedLivestock: livestock }, null, 2) }],
601
+ };
602
+ }
603
+ case 'create_barn': {
604
+ const barnName = requireString(args, 'name');
605
+ const host = requireString(args, 'host');
606
+ const user = requireString(args, 'user');
607
+ const port = optionalNumber(args, 'port') ?? 22;
608
+ const identity_file = requireString(args, 'identity_file');
609
+ // Cannot create a barn named 'local' - it's reserved
610
+ if (barnName === 'local') {
611
+ return {
612
+ content: [{ type: 'text', text: `Cannot create barn named 'local': this name is reserved for the local machine` }],
613
+ isError: true,
614
+ };
615
+ }
616
+ const existing = loadBarn(barnName);
617
+ if (existing) {
618
+ return {
619
+ content: [{ type: 'text', text: `Barn already exists: ${barnName}` }],
620
+ isError: true,
621
+ };
622
+ }
623
+ const barn = {
624
+ name: barnName,
625
+ host,
626
+ user,
627
+ port,
628
+ identity_file,
629
+ critters: [],
630
+ };
631
+ saveBarn(barn);
632
+ return {
633
+ content: [{ type: 'text', text: `Created barn: ${barn.name} (${barn.user}@${barn.host})` }],
634
+ };
635
+ }
636
+ case 'update_barn': {
637
+ const barnName = requireString(args, 'name');
638
+ const host = optionalString(args, 'host');
639
+ const user = optionalString(args, 'user');
640
+ const port = optionalNumber(args, 'port');
641
+ const identity_file = optionalString(args, 'identity_file');
642
+ // Cannot update the local barn
643
+ if (barnName === 'local') {
644
+ return {
645
+ content: [{ type: 'text', text: `Cannot update 'local' barn: it represents the local machine` }],
646
+ isError: true,
647
+ };
648
+ }
649
+ const barn = loadBarn(barnName);
650
+ if (!barn) {
651
+ return {
652
+ content: [{ type: 'text', text: `Barn not found: ${barnName}` }],
653
+ isError: true,
654
+ };
655
+ }
656
+ if (host !== undefined)
657
+ barn.host = host;
658
+ if (user !== undefined)
659
+ barn.user = user;
660
+ if (port !== undefined)
661
+ barn.port = port;
662
+ if (identity_file !== undefined)
663
+ barn.identity_file = identity_file;
664
+ saveBarn(barn);
665
+ return {
666
+ content: [{ type: 'text', text: `Updated barn: ${barn.name}` }],
667
+ };
668
+ }
669
+ case 'delete_barn': {
670
+ const barnName = requireString(args, 'name');
671
+ const confirm = requireString(args, 'confirm');
672
+ // Cannot delete the local barn
673
+ if (barnName === 'local') {
674
+ return {
675
+ content: [{ type: 'text', text: `Cannot delete 'local' barn: it represents the local machine and is always available` }],
676
+ isError: true,
677
+ };
678
+ }
679
+ if (confirm !== barnName) {
680
+ return {
681
+ content: [{ type: 'text', text: `Confirmation does not match. To delete, confirm must equal "${barnName}"` }],
682
+ isError: true,
683
+ };
684
+ }
685
+ // Check if any livestock references this barn
686
+ const livestock = getLivestockForBarn(barnName);
687
+ if (livestock.length > 0) {
688
+ const refs = livestock.map((l) => `${l.project.name}/${l.livestock.name}`).join(', ');
689
+ return {
690
+ content: [{ type: 'text', text: `Cannot delete barn: still referenced by livestock: ${refs}` }],
691
+ isError: true,
692
+ };
693
+ }
694
+ const deleted = deleteBarn(barnName);
695
+ if (!deleted) {
696
+ return {
697
+ content: [{ type: 'text', text: `Barn not found: ${barnName}` }],
698
+ isError: true,
699
+ };
700
+ }
701
+ return {
702
+ content: [{ type: 'text', text: `Deleted barn: ${barnName}` }],
703
+ };
704
+ }
705
+ // Wiki operations
706
+ case 'get_wiki': {
707
+ const projectName = requireString(args, 'project');
708
+ const project = loadProject(projectName);
709
+ if (!project) {
710
+ return {
711
+ content: [{ type: 'text', text: `Project not found: ${projectName}` }],
712
+ isError: true,
713
+ };
714
+ }
715
+ return {
716
+ content: [{ type: 'text', text: JSON.stringify(project.wiki || [], null, 2) }],
717
+ };
718
+ }
719
+ case 'add_wiki_section': {
720
+ const projectName = requireString(args, 'project');
721
+ const title = requireString(args, 'title');
722
+ const content = requireString(args, 'content');
723
+ const project = loadProject(projectName);
724
+ if (!project) {
725
+ return {
726
+ content: [{ type: 'text', text: `Project not found: ${projectName}` }],
727
+ isError: true,
728
+ };
729
+ }
730
+ const section = { title, content };
731
+ project.wiki = project.wiki || [];
732
+ // Check if section with this title already exists
733
+ const existingIdx = project.wiki.findIndex((s) => s.title === section.title);
734
+ if (existingIdx >= 0) {
735
+ return {
736
+ content: [{ type: 'text', text: `Wiki section already exists: ${section.title}. Use update_wiki_section to modify.` }],
737
+ isError: true,
738
+ };
739
+ }
740
+ project.wiki.push(section);
741
+ saveProject(project);
742
+ return {
743
+ content: [{ type: 'text', text: `Added wiki section '${section.title}' to project '${project.name}'` }],
744
+ };
745
+ }
746
+ case 'update_wiki_section': {
747
+ const projectName = requireString(args, 'project');
748
+ const title = requireString(args, 'title');
749
+ const newTitle = optionalString(args, 'new_title');
750
+ const content = optionalString(args, 'content');
751
+ const project = loadProject(projectName);
752
+ if (!project) {
753
+ return {
754
+ content: [{ type: 'text', text: `Project not found: ${projectName}` }],
755
+ isError: true,
756
+ };
757
+ }
758
+ project.wiki = project.wiki || [];
759
+ const sectionIdx = project.wiki.findIndex((s) => s.title === title);
760
+ if (sectionIdx < 0) {
761
+ return {
762
+ content: [{ type: 'text', text: `Wiki section not found: ${title}` }],
763
+ isError: true,
764
+ };
765
+ }
766
+ if (newTitle !== undefined) {
767
+ project.wiki[sectionIdx].title = newTitle;
768
+ }
769
+ if (content !== undefined) {
770
+ project.wiki[sectionIdx].content = content;
771
+ }
772
+ saveProject(project);
773
+ return {
774
+ content: [{ type: 'text', text: `Updated wiki section '${title}' in project '${project.name}'` }],
775
+ };
776
+ }
777
+ case 'delete_wiki_section': {
778
+ const projectName = requireString(args, 'project');
779
+ const title = requireString(args, 'title');
780
+ const project = loadProject(projectName);
781
+ if (!project) {
782
+ return {
783
+ content: [{ type: 'text', text: `Project not found: ${projectName}` }],
784
+ isError: true,
785
+ };
786
+ }
787
+ project.wiki = project.wiki || [];
788
+ const originalLength = project.wiki.length;
789
+ project.wiki = project.wiki.filter((s) => s.title !== title);
790
+ if (project.wiki.length === originalLength) {
791
+ return {
792
+ content: [{ type: 'text', text: `Wiki section not found: ${title}` }],
793
+ isError: true,
794
+ };
795
+ }
796
+ saveProject(project);
797
+ return {
798
+ content: [{ type: 'text', text: `Deleted wiki section '${title}' from project '${project.name}'` }],
799
+ };
800
+ }
801
+ default:
802
+ return {
803
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
804
+ isError: true,
805
+ };
806
+ }
807
+ }
808
+ catch (err) {
809
+ // Validation errors from requireString etc.
810
+ return {
811
+ content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }],
812
+ isError: true,
813
+ };
814
+ }
815
+ });
816
+ // ============================================================================
817
+ // Start server
818
+ // ============================================================================
819
+ async function main() {
820
+ ensureConfigDirs();
821
+ const transport = new StdioServerTransport();
822
+ await server.connect(transport);
823
+ console.error('Yeehaw MCP server running');
824
+ }
825
+ main().catch(console.error);