contree-mcp 0.1.0__py3-none-any.whl

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 (46) hide show
  1. contree_mcp/__init__.py +0 -0
  2. contree_mcp/__main__.py +25 -0
  3. contree_mcp/app.py +240 -0
  4. contree_mcp/arguments.py +35 -0
  5. contree_mcp/auth/__init__.py +2 -0
  6. contree_mcp/auth/registry.py +236 -0
  7. contree_mcp/backend_types.py +301 -0
  8. contree_mcp/cache.py +208 -0
  9. contree_mcp/client.py +711 -0
  10. contree_mcp/context.py +53 -0
  11. contree_mcp/docs.py +1203 -0
  12. contree_mcp/file_cache.py +381 -0
  13. contree_mcp/prompts.py +238 -0
  14. contree_mcp/py.typed +0 -0
  15. contree_mcp/resources/__init__.py +17 -0
  16. contree_mcp/resources/guide.py +715 -0
  17. contree_mcp/resources/image_lineage.py +46 -0
  18. contree_mcp/resources/image_ls.py +32 -0
  19. contree_mcp/resources/import_operation.py +52 -0
  20. contree_mcp/resources/instance_operation.py +52 -0
  21. contree_mcp/resources/read_file.py +33 -0
  22. contree_mcp/resources/static.py +12 -0
  23. contree_mcp/server.py +77 -0
  24. contree_mcp/tools/__init__.py +39 -0
  25. contree_mcp/tools/cancel_operation.py +36 -0
  26. contree_mcp/tools/download.py +128 -0
  27. contree_mcp/tools/get_guide.py +54 -0
  28. contree_mcp/tools/get_image.py +30 -0
  29. contree_mcp/tools/get_operation.py +26 -0
  30. contree_mcp/tools/import_image.py +99 -0
  31. contree_mcp/tools/list_files.py +80 -0
  32. contree_mcp/tools/list_images.py +50 -0
  33. contree_mcp/tools/list_operations.py +46 -0
  34. contree_mcp/tools/read_file.py +47 -0
  35. contree_mcp/tools/registry_auth.py +71 -0
  36. contree_mcp/tools/registry_token_obtain.py +80 -0
  37. contree_mcp/tools/rsync.py +46 -0
  38. contree_mcp/tools/run.py +97 -0
  39. contree_mcp/tools/set_tag.py +31 -0
  40. contree_mcp/tools/upload.py +50 -0
  41. contree_mcp/tools/wait_operations.py +79 -0
  42. contree_mcp-0.1.0.dist-info/METADATA +450 -0
  43. contree_mcp-0.1.0.dist-info/RECORD +46 -0
  44. contree_mcp-0.1.0.dist-info/WHEEL +4 -0
  45. contree_mcp-0.1.0.dist-info/entry_points.txt +2 -0
  46. contree_mcp-0.1.0.dist-info/licenses/LICENSE +176 -0
contree_mcp/docs.py ADDED
@@ -0,0 +1,1203 @@
1
+ """HTML documentation page generator for Contree MCP server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html
6
+ from collections.abc import Mapping
7
+ from typing import Any
8
+
9
+ CSS_STYLES = """\
10
+ :root {
11
+ --bg-primary: #0d1117;
12
+ --bg-secondary: #161b22;
13
+ --bg-tertiary: #21262d;
14
+ --text-primary: #e6edf3;
15
+ --text-secondary: #8b949e;
16
+ --text-muted: #6e7681;
17
+ --border-color: #30363d;
18
+ --accent: #58a6ff;
19
+ --accent-hover: #79b8ff;
20
+ --success: #3fb950;
21
+ --warning: #d29922;
22
+ --code-bg: #343a42;
23
+ }
24
+
25
+ * {
26
+ box-sizing: border-box;
27
+ margin: 0;
28
+ padding: 0;
29
+ }
30
+
31
+ body {
32
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
33
+ background: var(--bg-primary);
34
+ color: var(--text-primary);
35
+ line-height: 1.6;
36
+ padding: 0;
37
+ min-height: 100vh;
38
+ }
39
+
40
+ header {
41
+ background: var(--bg-secondary);
42
+ border-bottom: 1px solid var(--border-color);
43
+ padding: 2rem;
44
+ text-align: center;
45
+ }
46
+
47
+ header h1 {
48
+ font-size: 2.5rem;
49
+ font-weight: 600;
50
+ margin-bottom: 0.5rem;
51
+ }
52
+
53
+ .tagline {
54
+ color: var(--text-secondary);
55
+ font-size: 1.1rem;
56
+ }
57
+
58
+ nav {
59
+ background: var(--bg-tertiary);
60
+ border-bottom: 1px solid var(--border-color);
61
+ padding: 0.75rem 2rem;
62
+ display: flex;
63
+ gap: 1.5rem;
64
+ flex-wrap: wrap;
65
+ justify-content: center;
66
+ position: sticky;
67
+ top: 0;
68
+ z-index: 100;
69
+ }
70
+
71
+ nav a {
72
+ color: var(--text-secondary);
73
+ text-decoration: none;
74
+ font-weight: 500;
75
+ transition: color 0.2s;
76
+ }
77
+
78
+ nav a:hover {
79
+ color: var(--accent);
80
+ }
81
+
82
+ main {
83
+ max-width: 1200px;
84
+ margin: 0 auto;
85
+ padding: 2rem;
86
+ }
87
+
88
+ section {
89
+ margin-bottom: 3rem;
90
+ }
91
+
92
+ h2 {
93
+ font-size: 1.75rem;
94
+ font-weight: 600;
95
+ margin-bottom: 1.5rem;
96
+ padding-bottom: 0.5rem;
97
+ border-bottom: 1px solid var(--border-color);
98
+ color: var(--text-primary);
99
+ }
100
+
101
+ h3 {
102
+ font-size: 1.25rem;
103
+ font-weight: 600;
104
+ margin-bottom: 0.75rem;
105
+ color: var(--text-primary);
106
+ }
107
+
108
+ .connection-box {
109
+ background: var(--bg-secondary);
110
+ border: 1px solid var(--border-color);
111
+ border-radius: 8px;
112
+ padding: 1.5rem;
113
+ margin-bottom: 1rem;
114
+ }
115
+
116
+ .connection-box p {
117
+ color: var(--text-secondary);
118
+ margin-bottom: 0.75rem;
119
+ }
120
+
121
+ .endpoint {
122
+ display: block;
123
+ background: var(--code-bg);
124
+ padding: 0.75rem 1rem;
125
+ border-radius: 6px;
126
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
127
+ font-size: 0.95rem;
128
+ color: var(--accent);
129
+ margin-bottom: 0.75rem;
130
+ }
131
+
132
+ .hint {
133
+ font-size: 0.9rem;
134
+ color: var(--text-muted);
135
+ }
136
+
137
+ .hint code {
138
+ background: var(--code-bg);
139
+ padding: 0.2rem 0.5rem;
140
+ border-radius: 4px;
141
+ font-size: 0.85rem;
142
+ }
143
+
144
+ .instructions-box {
145
+ background: var(--bg-secondary);
146
+ border: 1px solid var(--border-color);
147
+ border-radius: 8px;
148
+ padding: 1.5rem;
149
+ }
150
+
151
+ .instructions-box pre {
152
+ background: var(--code-bg);
153
+ padding: 1rem;
154
+ border-radius: 6px;
155
+ overflow-x: auto;
156
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
157
+ font-size: 0.9rem;
158
+ line-height: 1.5;
159
+ color: var(--text-secondary);
160
+ }
161
+
162
+ .tools-table {
163
+ width: 100%;
164
+ border-collapse: collapse;
165
+ background: var(--bg-secondary);
166
+ border-radius: 8px;
167
+ overflow: hidden;
168
+ }
169
+
170
+ .tools-table th {
171
+ background: var(--bg-tertiary);
172
+ padding: 1rem;
173
+ text-align: left;
174
+ font-weight: 600;
175
+ border-bottom: 1px solid var(--border-color);
176
+ }
177
+
178
+ .tools-table td {
179
+ padding: 1rem;
180
+ border-bottom: 1px solid var(--border-color);
181
+ vertical-align: top;
182
+ }
183
+
184
+ .tools-table tr:last-child td {
185
+ border-bottom: none;
186
+ }
187
+
188
+ .tool-name {
189
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
190
+ color: var(--accent);
191
+ font-weight: 500;
192
+ }
193
+
194
+ .tool-desc {
195
+ color: var(--text-secondary);
196
+ font-size: 0.95rem;
197
+ }
198
+
199
+ .tool-params {
200
+ font-size: 0.85rem;
201
+ color: var(--text-muted);
202
+ margin-top: 0.5rem;
203
+ }
204
+
205
+ .param-name {
206
+ color: var(--success);
207
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
208
+ }
209
+
210
+ .param-required {
211
+ color: var(--warning);
212
+ font-size: 0.75rem;
213
+ margin-left: 0.25rem;
214
+ }
215
+
216
+ .resources-table {
217
+ width: 100%;
218
+ border-collapse: collapse;
219
+ background: var(--bg-secondary);
220
+ border-radius: 8px;
221
+ overflow: hidden;
222
+ }
223
+
224
+ .resources-table th {
225
+ background: var(--bg-tertiary);
226
+ padding: 1rem;
227
+ text-align: left;
228
+ font-weight: 600;
229
+ border-bottom: 1px solid var(--border-color);
230
+ }
231
+
232
+ .resources-table td {
233
+ padding: 1rem;
234
+ border-bottom: 1px solid var(--border-color);
235
+ vertical-align: top;
236
+ }
237
+
238
+ .resources-table tr:last-child td {
239
+ border-bottom: none;
240
+ }
241
+
242
+ .resource-name {
243
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
244
+ color: var(--accent);
245
+ font-weight: 500;
246
+ }
247
+
248
+ .uri-template {
249
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
250
+ font-size: 0.9rem;
251
+ color: var(--text-secondary);
252
+ background: var(--code-bg);
253
+ padding: 0.25rem 0.5rem;
254
+ border-radius: 4px;
255
+ }
256
+
257
+ .guide-item {
258
+ background: var(--bg-secondary);
259
+ border: 1px solid var(--border-color);
260
+ border-radius: 8px;
261
+ margin-bottom: 1rem;
262
+ overflow: hidden;
263
+ }
264
+
265
+ .guide-header {
266
+ display: flex;
267
+ align-items: center;
268
+ justify-content: space-between;
269
+ padding: 1rem 1.5rem;
270
+ cursor: pointer;
271
+ background: var(--bg-secondary);
272
+ transition: background 0.2s;
273
+ }
274
+
275
+ .guide-header:hover {
276
+ background: var(--bg-tertiary);
277
+ }
278
+
279
+ .guide-title {
280
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
281
+ color: var(--accent);
282
+ font-weight: 500;
283
+ }
284
+
285
+ .guide-toggle {
286
+ color: var(--text-muted);
287
+ font-size: 1.25rem;
288
+ transition: transform 0.2s;
289
+ }
290
+
291
+ .guide-item.open .guide-toggle {
292
+ transform: rotate(180deg);
293
+ }
294
+
295
+ .guide-content {
296
+ display: none;
297
+ padding: 1.5rem;
298
+ border-top: 1px solid var(--border-color);
299
+ background: var(--bg-primary);
300
+ }
301
+
302
+ .guide-item.open .guide-content {
303
+ display: block;
304
+ }
305
+
306
+ .guide-content h1 {
307
+ font-size: 1.5rem;
308
+ margin-bottom: 1rem;
309
+ color: var(--text-primary);
310
+ }
311
+
312
+ .guide-content h2 {
313
+ font-size: 1.25rem;
314
+ margin: 1.5rem 0 1rem 0;
315
+ padding-bottom: 0.25rem;
316
+ border-bottom: 1px solid var(--border-color);
317
+ }
318
+
319
+ .guide-content h3 {
320
+ font-size: 1.1rem;
321
+ margin: 1.25rem 0 0.75rem 0;
322
+ }
323
+
324
+ .guide-content p {
325
+ margin-bottom: 1rem;
326
+ color: var(--text-secondary);
327
+ }
328
+
329
+ .guide-content ul, .guide-content ol {
330
+ margin-bottom: 1rem;
331
+ padding-left: 1.5rem;
332
+ color: var(--text-secondary);
333
+ }
334
+
335
+ .guide-content li {
336
+ margin-bottom: 0.5rem;
337
+ }
338
+
339
+ .guide-content code {
340
+ background: var(--code-bg);
341
+ padding: 0.2rem 0.5rem;
342
+ border-radius: 4px;
343
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
344
+ font-size: 0.9rem;
345
+ }
346
+
347
+ .guide-content .code-block pre,
348
+ .guide-content pre {
349
+ background: var(--code-bg);
350
+ border: 1px solid var(--border-color);
351
+ padding: 1rem;
352
+ padding-right: 3rem;
353
+ border-radius: 6px;
354
+ overflow-x: auto;
355
+ margin: 0;
356
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', Consolas, monospace;
357
+ font-size: 0.85rem;
358
+ line-height: 1.6;
359
+ color: var(--text-primary);
360
+ }
361
+
362
+ .guide-content .code-block {
363
+ margin-bottom: 1rem;
364
+ }
365
+
366
+ .guide-content pre code {
367
+ background: none;
368
+ padding: 0;
369
+ }
370
+
371
+ .guide-content table {
372
+ width: 100%;
373
+ border-collapse: collapse;
374
+ margin-bottom: 1rem;
375
+ }
376
+
377
+ .guide-content th, .guide-content td {
378
+ padding: 0.75rem;
379
+ border: 1px solid var(--border-color);
380
+ text-align: left;
381
+ }
382
+
383
+ .guide-content th {
384
+ background: var(--bg-tertiary);
385
+ font-weight: 600;
386
+ }
387
+
388
+ .guide-content strong {
389
+ color: var(--text-primary);
390
+ }
391
+
392
+ footer {
393
+ background: var(--bg-secondary);
394
+ border-top: 1px solid var(--border-color);
395
+ padding: 2rem;
396
+ text-align: center;
397
+ color: var(--text-muted);
398
+ margin-top: 2rem;
399
+ }
400
+
401
+ @media (max-width: 768px) {
402
+ header {
403
+ padding: 1.5rem 1rem;
404
+ }
405
+
406
+ header h1 {
407
+ font-size: 1.75rem;
408
+ }
409
+
410
+ nav {
411
+ padding: 0.75rem 1rem;
412
+ gap: 1rem;
413
+ }
414
+
415
+ main {
416
+ padding: 1rem;
417
+ }
418
+
419
+ .tools-table, .resources-table {
420
+ display: block;
421
+ overflow-x: auto;
422
+ }
423
+
424
+ .connection-box {
425
+ padding: 1rem;
426
+ }
427
+ }
428
+
429
+ @media (prefers-color-scheme: light) {
430
+ :root {
431
+ --bg-primary: #ffffff;
432
+ --bg-secondary: #f6f8fa;
433
+ --bg-tertiary: #eaeef2;
434
+ --text-primary: #1f2328;
435
+ --text-secondary: #656d76;
436
+ --text-muted: #8c959f;
437
+ --border-color: #d0d7de;
438
+ --accent: #0969da;
439
+ --accent-hover: #0550ae;
440
+ --success: #1a7f37;
441
+ --warning: #9a6700;
442
+ --code-bg: #eaeff5;
443
+ }
444
+ }
445
+
446
+ .setup-box {
447
+ background: var(--bg-secondary);
448
+ border: 1px solid var(--border-color);
449
+ border-radius: 8px;
450
+ margin-bottom: 1.5rem;
451
+ overflow: hidden;
452
+ }
453
+
454
+ .setup-box h3 {
455
+ padding: 1rem 1.5rem;
456
+ margin: 0;
457
+ border-bottom: 1px solid var(--border-color);
458
+ display: flex;
459
+ align-items: center;
460
+ gap: 0.5rem;
461
+ }
462
+
463
+ .setup-box h3 .icon {
464
+ font-size: 1.25rem;
465
+ }
466
+
467
+ .tabs {
468
+ display: flex;
469
+ border-bottom: 1px solid var(--border-color);
470
+ background: var(--bg-tertiary);
471
+ }
472
+
473
+ .tab-btn {
474
+ padding: 0.75rem 1.5rem;
475
+ border: none;
476
+ background: transparent;
477
+ color: var(--text-secondary);
478
+ cursor: pointer;
479
+ font-size: 0.9rem;
480
+ font-weight: 500;
481
+ transition: all 0.2s;
482
+ border-bottom: 2px solid transparent;
483
+ margin-bottom: -1px;
484
+ }
485
+
486
+ .tab-btn:hover {
487
+ color: var(--text-primary);
488
+ background: var(--bg-secondary);
489
+ }
490
+
491
+ .tab-btn.active {
492
+ color: var(--accent);
493
+ border-bottom-color: var(--accent);
494
+ background: var(--bg-secondary);
495
+ }
496
+
497
+ .tab-content {
498
+ display: none;
499
+ padding: 1.5rem;
500
+ }
501
+
502
+ .tab-content.active {
503
+ display: block;
504
+ }
505
+
506
+ .tab-content p {
507
+ color: var(--text-secondary);
508
+ margin-bottom: 1rem;
509
+ }
510
+
511
+ .tab-content code {
512
+ background: var(--code-bg);
513
+ padding: 0.2rem 0.4rem;
514
+ border-radius: 4px;
515
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
516
+ font-size: 0.85rem;
517
+ }
518
+
519
+ .tab-content .note {
520
+ font-size: 0.85rem;
521
+ color: var(--text-muted);
522
+ padding: 0.75rem 1rem;
523
+ background: var(--bg-tertiary);
524
+ border-radius: 6px;
525
+ border-left: 3px solid var(--accent);
526
+ }
527
+
528
+ .code-block {
529
+ position: relative;
530
+ margin-bottom: 1rem;
531
+ }
532
+
533
+ .code-block pre {
534
+ background: var(--code-bg);
535
+ border: 1px solid var(--border-color);
536
+ padding: 1rem;
537
+ padding-right: 3rem;
538
+ border-radius: 6px;
539
+ overflow-x: auto;
540
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', Consolas, monospace;
541
+ font-size: 0.85rem;
542
+ line-height: 1.6;
543
+ color: var(--text-primary);
544
+ margin: 0;
545
+ }
546
+
547
+ .code-block pre .comment {
548
+ color: var(--text-muted);
549
+ }
550
+
551
+ .code-block .copy-btn {
552
+ position: absolute;
553
+ top: 0.5rem;
554
+ right: 0.5rem;
555
+ background: var(--bg-tertiary);
556
+ border: 1px solid var(--border-color);
557
+ border-radius: 4px;
558
+ padding: 0.25rem 0.5rem;
559
+ cursor: pointer;
560
+ font-size: 0.75rem;
561
+ color: var(--text-secondary);
562
+ transition: all 0.2s;
563
+ opacity: 0;
564
+ }
565
+
566
+ .code-block:hover .copy-btn {
567
+ opacity: 1;
568
+ }
569
+
570
+ .code-block .copy-btn:hover {
571
+ background: var(--accent);
572
+ color: white;
573
+ border-color: var(--accent);
574
+ }
575
+
576
+ .code-block .copy-btn.copied {
577
+ background: var(--success);
578
+ color: white;
579
+ border-color: var(--success);
580
+ }
581
+ """
582
+
583
+ JS_SCRIPTS = """\
584
+ document.querySelectorAll('.guide-header').forEach(header => {
585
+ header.addEventListener('click', () => {
586
+ const item = header.parentElement;
587
+ item.classList.toggle('open');
588
+ });
589
+ });
590
+
591
+ document.querySelectorAll('.tab-btn').forEach(btn => {
592
+ btn.addEventListener('click', () => {
593
+ const container = btn.closest('.setup-box');
594
+ const mode = btn.dataset.mode;
595
+ container.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
596
+ container.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
597
+ btn.classList.add('active');
598
+ container.querySelector('.tab-content[data-mode=\"' + mode + '\"]').classList.add('active');
599
+ });
600
+ });
601
+
602
+ // Wrap pre elements in code-block with copy button
603
+ document.querySelectorAll('.tab-content pre, .guide-content pre').forEach(pre => {
604
+ if (pre.parentElement.classList.contains('code-block')) return;
605
+ const wrapper = document.createElement('div');
606
+ wrapper.className = 'code-block';
607
+ pre.parentNode.insertBefore(wrapper, pre);
608
+ wrapper.appendChild(pre);
609
+
610
+ const copyBtn = document.createElement('button');
611
+ copyBtn.className = 'copy-btn';
612
+ copyBtn.textContent = 'Copy';
613
+ copyBtn.addEventListener('click', async () => {
614
+ const text = pre.textContent;
615
+ try {
616
+ await navigator.clipboard.writeText(text);
617
+ copyBtn.textContent = 'Copied!';
618
+ copyBtn.classList.add('copied');
619
+ setTimeout(() => {
620
+ copyBtn.textContent = 'Copy';
621
+ copyBtn.classList.remove('copied');
622
+ }, 2000);
623
+ } catch (err) {
624
+ copyBtn.textContent = 'Failed';
625
+ setTimeout(() => { copyBtn.textContent = 'Copy'; }, 2000);
626
+ }
627
+ });
628
+ wrapper.appendChild(copyBtn);
629
+ });
630
+ """
631
+
632
+
633
+ def generate_docs_html(
634
+ server_instructions: str,
635
+ tools: list[Any],
636
+ templates: list[Any],
637
+ guides: Mapping[str, str],
638
+ http_port: int = 8080,
639
+ ) -> str:
640
+ """Generate HTML documentation page.
641
+
642
+ Args:
643
+ server_instructions: The SERVER_INSTRUCTIONS from server.py
644
+ tools: List of registered MCP tools
645
+ templates: List of registered resource templates
646
+ guides: Dict mapping section names to guide content
647
+ http_port: The HTTP port the server is running on
648
+
649
+ Returns:
650
+ Complete HTML document as string
651
+ """
652
+ tools_html = _render_tools_section(tools)
653
+ resources_html = _render_resources_section(templates)
654
+ guides_html = _render_guides_section(guides)
655
+ instructions_html = _render_instructions_section(server_instructions)
656
+
657
+ return f"""\
658
+ <!DOCTYPE html>
659
+ <html lang="en">
660
+ <head>
661
+ <meta charset="UTF-8">
662
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
663
+ <title>Contree MCP Server</title>
664
+ <style>
665
+ {CSS_STYLES}
666
+ </style>
667
+ </head>
668
+ <body>
669
+ <header>
670
+ <h1>Contree MCP Server</h1>
671
+ <p class="tagline">Container execution for AI agents</p>
672
+ </header>
673
+
674
+ <nav>
675
+ <a href="#setup">Setup</a>
676
+ <a href="#instructions">Instructions</a>
677
+ <a href="#tools">Tools</a>
678
+ <a href="#resources">Resources</a>
679
+ <a href="#guides">Guides</a>
680
+ </nav>
681
+
682
+ <main>
683
+ <section id="setup">
684
+ <h2>Setup</h2>
685
+
686
+ <div class="setup-box">
687
+ <h3><span class="icon">🔑</span> Authentication (Required First)</h3>
688
+ <div class="tab-content active" style="display: block;">
689
+ <p>Create a config file to store your API token securely:</p>
690
+ <pre>mkdir -p ~/.config/contree</pre>
691
+ <p>Add your credentials:</p>
692
+ <pre>cat &gt; ~/.config/contree/mcp.ini &lt;&lt; 'EOF'
693
+ [DEFAULT]
694
+ url = https://contree.dev/
695
+ token = your-token-here
696
+ EOF</pre>
697
+ <p>Alternatively, use a custom config location:</p>
698
+ <pre>export CONTREE_MCP_CONFIG="/path/to/custom/config.ini"</pre>
699
+ <p class="note">With token in config, MCP configs below don't need env vars.</p>
700
+ </div>
701
+ </div>
702
+
703
+ <div class="setup-box">
704
+ <h3><span class="icon">🖥️</span> Claude Code</h3>
705
+ <div class="tabs">
706
+ <button class="tab-btn active" data-mode="stdio">Stdio</button>
707
+ <button class="tab-btn" data-mode="http">HTTP</button>
708
+ </div>
709
+ <div class="tab-content active" data-mode="stdio">
710
+ <p>Using CLI:</p>
711
+ <pre>claude mcp add contree -- uvx contree-mcp</pre>
712
+ <p>Or add to config file (<code>~/.claude.json</code> or <code>.mcp.json</code>):</p>
713
+ <pre>{{
714
+ "mcpServers": {{
715
+ "contree": {{
716
+ "type": "stdio",
717
+ "command": "uvx",
718
+ "args": ["contree-mcp"]
719
+ }}
720
+ }}
721
+ }}</pre>
722
+ <p>To use a custom config path, add <code>env</code>:</p>
723
+ <pre>{{
724
+ "mcpServers": {{
725
+ "contree": {{
726
+ "type": "stdio",
727
+ "command": "uvx",
728
+ "args": ["contree-mcp"],
729
+ "env": {{
730
+ "CONTREE_MCP_CONFIG": "/path/to/config.ini"
731
+ }}
732
+ }}
733
+ }}
734
+ }}</pre>
735
+ <p class="note">Verify with <code>claude mcp list</code></p>
736
+ </div>
737
+ <div class="tab-content" data-mode="http">
738
+ <p>Start the HTTP server:</p>
739
+ <pre>uvx contree-mcp --mode http --port {http_port}</pre>
740
+ <p>Using CLI:</p>
741
+ <pre>claude mcp add contree --transport http http://localhost:{http_port}/mcp</pre>
742
+ <p>Or add to config file:</p>
743
+ <pre>{{
744
+ "mcpServers": {{
745
+ "contree": {{
746
+ "type": "http",
747
+ "url": "http://localhost:{http_port}/mcp"
748
+ }}
749
+ }}
750
+ }}</pre>
751
+ </div>
752
+ </div>
753
+
754
+ <div class="setup-box">
755
+ <h3><span class="icon">⌨️</span> Codex CLI</h3>
756
+ <div class="tabs">
757
+ <button class="tab-btn active" data-mode="stdio">Stdio</button>
758
+ <button class="tab-btn" data-mode="http">HTTP</button>
759
+ </div>
760
+ <div class="tab-content active" data-mode="stdio">
761
+ <p>Using CLI:</p>
762
+ <pre>codex mcp add contree -- uvx contree-mcp</pre>
763
+ <p>Or add to config file (<code>~/.codex/config.toml</code>):</p>
764
+ <pre>[mcp_servers.contree]
765
+ command = "uvx"
766
+ args = ["contree-mcp"]</pre>
767
+ <p>To use a custom config path, add <code>env</code>:</p>
768
+ <pre>[mcp_servers.contree]
769
+ command = "uvx"
770
+ args = ["contree-mcp"]
771
+ env = {{ CONTREE_MCP_CONFIG = "/path/to/config.ini" }}</pre>
772
+ <p class="note">Use <code>mcp_servers</code> (underscore).</p>
773
+ </div>
774
+ <div class="tab-content" data-mode="http">
775
+ <p>Start the HTTP server:</p>
776
+ <pre>uvx contree-mcp --mode http --port {http_port}</pre>
777
+ <p>Add to config file (<code>~/.codex/config.toml</code>):</p>
778
+ <pre>[mcp_servers.contree]
779
+ url = "http://localhost:{http_port}/mcp"</pre>
780
+ </div>
781
+ </div>
782
+
783
+ <div class="setup-box">
784
+ <h3><span class="icon">🔓</span> OpenCode</h3>
785
+ <div class="tabs">
786
+ <button class="tab-btn active" data-mode="stdio">Stdio</button>
787
+ <button class="tab-btn" data-mode="http">HTTP</button>
788
+ </div>
789
+ <div class="tab-content active" data-mode="stdio">
790
+ <p>Using CLI (interactive TUI wizard):</p>
791
+ <pre>opencode mcp add</pre>
792
+ <p>Or add to config file (<code>~/.config/opencode/opencode.json</code>):</p>
793
+ <pre>{{
794
+ "mcp": {{
795
+ "contree": {{
796
+ "type": "local",
797
+ "command": ["uvx", "contree-mcp"],
798
+ "enabled": true
799
+ }}
800
+ }}
801
+ }}</pre>
802
+ <p>To use a custom config path, add <code>environment</code>:</p>
803
+ <pre>{{
804
+ "mcp": {{
805
+ "contree": {{
806
+ "type": "local",
807
+ "command": ["uvx", "contree-mcp"],
808
+ "environment": {{
809
+ "CONTREE_MCP_CONFIG": "/path/to/config.ini"
810
+ }},
811
+ "enabled": true
812
+ }}
813
+ }}
814
+ }}</pre>
815
+ <p class="note">Verify with <code>opencode mcp list</code></p>
816
+ </div>
817
+ <div class="tab-content" data-mode="http">
818
+ <p>Start the HTTP server:</p>
819
+ <pre>uvx contree-mcp --mode http --port {http_port}</pre>
820
+ <p>Add to config file (<code>~/.config/opencode/opencode.json</code>):</p>
821
+ <pre>{{
822
+ "mcp": {{
823
+ "contree": {{
824
+ "type": "remote",
825
+ "url": "http://localhost:{http_port}/mcp",
826
+ "enabled": true
827
+ }}
828
+ }}
829
+ }}</pre>
830
+ </div>
831
+ </div>
832
+
833
+ </section>
834
+
835
+ {instructions_html}
836
+
837
+ {tools_html}
838
+
839
+ {resources_html}
840
+
841
+ {guides_html}
842
+ </main>
843
+
844
+ <footer>
845
+ <p>Contree MCP Server &mdash; Container execution for AI agents</p>
846
+ </footer>
847
+
848
+ <script>
849
+ {JS_SCRIPTS}
850
+ </script>
851
+ </body>
852
+ </html>
853
+ """
854
+
855
+
856
+ def _render_instructions_section(instructions: str) -> str:
857
+ """Render the server instructions section."""
858
+ content_html = _markdown_to_html(instructions)
859
+ return f"""\
860
+ <section id="instructions">
861
+ <h2>Server Instructions</h2>
862
+ <div class="guide-content" style="display: block; background: var(--bg-secondary);
863
+ border: 1px solid var(--border-color); border-radius: 8px;">
864
+ {content_html}
865
+ </div>
866
+ </section>
867
+ """
868
+
869
+
870
+ def _render_tools_section(tools: list[Any]) -> str:
871
+ """Render the tools reference section."""
872
+ rows = []
873
+ for tool in sorted(tools, key=lambda t: t.name):
874
+ desc = _get_first_paragraph(tool.description or "")
875
+ # Handle both mcp.types.Tool (inputSchema) and FastMCP Tool (parameters)
876
+ schema = getattr(tool, "inputSchema", None) or getattr(tool, "parameters", {})
877
+ params_html = _render_tool_params(schema)
878
+
879
+ rows.append(f"""\
880
+ <tr>
881
+ <td><span class="tool-name">{html.escape(tool.name)}</span></td>
882
+ <td>
883
+ <div class="tool-desc">{html.escape(desc)}</div>
884
+ {params_html}
885
+ </td>
886
+ </tr>""")
887
+
888
+ return f"""\
889
+ <section id="tools">
890
+ <h2>Tools Reference</h2>
891
+ <table class="tools-table">
892
+ <thead>
893
+ <tr>
894
+ <th style="width: 200px;">Tool</th>
895
+ <th>Description &amp; Parameters</th>
896
+ </tr>
897
+ </thead>
898
+ <tbody>
899
+ {"".join(rows)}
900
+ </tbody>
901
+ </table>
902
+ </section>
903
+ """
904
+
905
+
906
+ def _render_tool_params(schema: dict[str, Any]) -> str:
907
+ """Render tool input parameters from JSON schema."""
908
+ properties = schema.get("properties", {})
909
+ required = set(schema.get("required", []))
910
+
911
+ if not properties:
912
+ return ""
913
+
914
+ # Handle $defs for nested types
915
+ defs = schema.get("$defs", {})
916
+
917
+ params = []
918
+ for name, prop in properties.items():
919
+ param_type = _get_param_type(prop, defs)
920
+ req_marker = '<span class="param-required">*</span>' if name in required else ""
921
+ param_html = f'<span class="param-name">{html.escape(name)}</span>'
922
+ params.append(f"{param_html}{req_marker}: {html.escape(param_type)}")
923
+
924
+ return f'<div class="tool-params">{", ".join(params)}</div>'
925
+
926
+
927
+ def _get_param_type(prop: dict[str, Any], defs: dict[str, Any]) -> str:
928
+ """Extract parameter type from JSON schema property."""
929
+ # Handle anyOf (optional types)
930
+ if "anyOf" in prop:
931
+ types = []
932
+ for option in prop["anyOf"]:
933
+ if option.get("type") == "null":
934
+ continue
935
+ if "$ref" in option:
936
+ ref_name = option["$ref"].split("/")[-1]
937
+ types.append(ref_name)
938
+ else:
939
+ types.append(option.get("type", "any"))
940
+ return " | ".join(types) if types else "any"
941
+
942
+ # Handle $ref
943
+ if "$ref" in prop:
944
+ ref = prop["$ref"]
945
+ return str(ref).split("/")[-1] if ref else "any"
946
+
947
+ # Handle enum
948
+ if "enum" in prop:
949
+ return " | ".join(f'"{v}"' for v in prop["enum"])
950
+
951
+ # Handle array
952
+ if prop.get("type") == "array":
953
+ items = prop.get("items", {})
954
+ item_type = _get_param_type(items, defs)
955
+ return f"array[{item_type}]"
956
+
957
+ # Handle object
958
+ if prop.get("type") == "object":
959
+ return "object"
960
+
961
+ # Basic type
962
+ type_val = prop.get("type", "any")
963
+ return str(type_val) if type_val else "any"
964
+
965
+
966
+ def _render_resources_section(templates: list[Any]) -> str:
967
+ """Render the resources reference section."""
968
+ rows = []
969
+ for template in sorted(templates, key=lambda t: t.name):
970
+ desc = _get_first_paragraph(template.description or "")
971
+ # Handle both mcp.types.ResourceTemplate (uriTemplate) and FastMCP (uri_template)
972
+ uri = getattr(template, "uriTemplate", None) or getattr(template, "uri_template", "") or ""
973
+ rows.append(f"""\
974
+ <tr>
975
+ <td><span class="resource-name">{html.escape(template.name)}</span></td>
976
+ <td><code class="uri-template">{html.escape(str(uri))}</code></td>
977
+ <td>{html.escape(desc)}</td>
978
+ </tr>""")
979
+
980
+ return f"""\
981
+ <section id="resources">
982
+ <h2>Resources Reference</h2>
983
+ <table class="resources-table">
984
+ <thead>
985
+ <tr>
986
+ <th style="width: 150px;">Resource</th>
987
+ <th style="width: 300px;">URI Template</th>
988
+ <th>Description</th>
989
+ </tr>
990
+ </thead>
991
+ <tbody>
992
+ {"".join(rows)}
993
+ </tbody>
994
+ </table>
995
+ </section>
996
+ """
997
+
998
+
999
+ def _render_guides_section(guides: Mapping[str, str]) -> str:
1000
+ """Render the guides section with collapsible items."""
1001
+ items = []
1002
+ for section_name in sorted(guides.keys()):
1003
+ content = guides[section_name]
1004
+ content_html = _markdown_to_html(content)
1005
+
1006
+ items.append(f"""\
1007
+ <div class="guide-item">
1008
+ <div class="guide-header">
1009
+ <span class="guide-title">contree://guide/{html.escape(section_name)}</span>
1010
+ <span class="guide-toggle">&#9662;</span>
1011
+ </div>
1012
+ <div class="guide-content">
1013
+ {content_html}
1014
+ </div>
1015
+ </div>""")
1016
+
1017
+ return f"""\
1018
+ <section id="guides">
1019
+ <h2>Guides</h2>
1020
+ {"".join(items)}
1021
+ </section>
1022
+ """
1023
+
1024
+
1025
+ def _get_first_paragraph(text: str) -> str:
1026
+ """Extract first paragraph from text."""
1027
+ lines: list[str] = []
1028
+ for line in text.split("\n"):
1029
+ line = line.strip()
1030
+ if not line and lines:
1031
+ break
1032
+ if line:
1033
+ lines.append(line)
1034
+ return " ".join(lines)
1035
+
1036
+
1037
+ def _markdown_to_html(md: str) -> str:
1038
+ """Convert simple markdown to HTML.
1039
+
1040
+ Handles: headers, code blocks, inline code, lists, tables, bold, paragraphs.
1041
+ """
1042
+ lines = md.split("\n")
1043
+ html_parts = []
1044
+ in_code_block = False
1045
+ code_block_lines: list[str] = []
1046
+ in_list = False
1047
+ list_type = ""
1048
+ in_table = False
1049
+ table_rows: list[str] = []
1050
+
1051
+ i = 0
1052
+ while i < len(lines):
1053
+ line = lines[i]
1054
+
1055
+ # Code blocks
1056
+ if line.startswith("```"):
1057
+ if in_code_block:
1058
+ code_content = html.escape("\n".join(code_block_lines))
1059
+ html_parts.append(f"<pre><code>{code_content}</code></pre>")
1060
+ code_block_lines = []
1061
+ in_code_block = False
1062
+ else:
1063
+ if in_list:
1064
+ html_parts.append(f"</{list_type}>")
1065
+ in_list = False
1066
+ in_code_block = True
1067
+ i += 1
1068
+ continue
1069
+
1070
+ if in_code_block:
1071
+ code_block_lines.append(line)
1072
+ i += 1
1073
+ continue
1074
+
1075
+ # Tables
1076
+ if "|" in line and line.strip().startswith("|"):
1077
+ if not in_table:
1078
+ if in_list:
1079
+ html_parts.append(f"</{list_type}>")
1080
+ in_list = False
1081
+ in_table = True
1082
+ table_rows = []
1083
+ table_rows.append(line)
1084
+ i += 1
1085
+ continue
1086
+ elif in_table:
1087
+ html_parts.append(_render_table(table_rows))
1088
+ in_table = False
1089
+ table_rows = []
1090
+
1091
+ # Headers
1092
+ if line.startswith("#"):
1093
+ if in_list:
1094
+ html_parts.append(f"</{list_type}>")
1095
+ in_list = False
1096
+ level = len(line) - len(line.lstrip("#"))
1097
+ level = min(level, 6)
1098
+ text = line[level:].strip()
1099
+ text = _inline_markdown(text)
1100
+ html_parts.append(f"<h{level}>{text}</h{level}>")
1101
+ i += 1
1102
+ continue
1103
+
1104
+ # Unordered lists
1105
+ if line.strip().startswith("- ") or line.strip().startswith("* "):
1106
+ if not in_list or list_type != "ul":
1107
+ if in_list:
1108
+ html_parts.append(f"</{list_type}>")
1109
+ html_parts.append("<ul>")
1110
+ in_list = True
1111
+ list_type = "ul"
1112
+ text = line.strip()[2:]
1113
+ text = _inline_markdown(text)
1114
+ html_parts.append(f"<li>{text}</li>")
1115
+ i += 1
1116
+ continue
1117
+
1118
+ # Ordered lists
1119
+ if line.strip() and line.strip()[0].isdigit() and ". " in line:
1120
+ if not in_list or list_type != "ol":
1121
+ if in_list:
1122
+ html_parts.append(f"</{list_type}>")
1123
+ html_parts.append("<ol>")
1124
+ in_list = True
1125
+ list_type = "ol"
1126
+ text = line.strip().split(". ", 1)[1]
1127
+ text = _inline_markdown(text)
1128
+ html_parts.append(f"<li>{text}</li>")
1129
+ i += 1
1130
+ continue
1131
+
1132
+ # End list if line doesn't continue it
1133
+ if in_list and line.strip() and not line.startswith(" "):
1134
+ html_parts.append(f"</{list_type}>")
1135
+ in_list = False
1136
+
1137
+ # Empty lines
1138
+ if not line.strip():
1139
+ i += 1
1140
+ continue
1141
+
1142
+ # Regular paragraph
1143
+ if in_list:
1144
+ html_parts.append(f"</{list_type}>")
1145
+ in_list = False
1146
+ text = _inline_markdown(line)
1147
+ html_parts.append(f"<p>{text}</p>")
1148
+ i += 1
1149
+
1150
+ # Close any open elements
1151
+ if in_list:
1152
+ html_parts.append(f"</{list_type}>")
1153
+ if in_table:
1154
+ html_parts.append(_render_table(table_rows))
1155
+ if in_code_block:
1156
+ code_content = html.escape("\n".join(code_block_lines))
1157
+ html_parts.append(f"<pre><code>{code_content}</code></pre>")
1158
+
1159
+ return "\n".join(html_parts)
1160
+
1161
+
1162
+ def _inline_markdown(text: str) -> str:
1163
+ """Convert inline markdown (code, bold, links) to HTML."""
1164
+ import re
1165
+
1166
+ # Escape HTML first
1167
+ text = html.escape(text)
1168
+
1169
+ # Inline code (backticks)
1170
+ text = re.sub(r"`([^`]+)`", r"<code>\1</code>", text)
1171
+
1172
+ # Bold (**text**)
1173
+ text = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", text)
1174
+
1175
+ return text
1176
+
1177
+
1178
+ def _render_table(rows: list[str]) -> str:
1179
+ """Render a markdown table to HTML."""
1180
+ if len(rows) < 2:
1181
+ return ""
1182
+
1183
+ def parse_row(row: str) -> list[str]:
1184
+ cells = row.strip().split("|")
1185
+ # Remove empty first/last from | delimiters
1186
+ if cells and not cells[0].strip():
1187
+ cells = cells[1:]
1188
+ if cells and not cells[-1].strip():
1189
+ cells = cells[:-1]
1190
+ return [c.strip() for c in cells]
1191
+
1192
+ header_cells = parse_row(rows[0])
1193
+
1194
+ # Skip separator row (|---|---|)
1195
+ data_rows = rows[2:] if len(rows) > 2 else []
1196
+
1197
+ header_html = "".join(f"<th>{_inline_markdown(c)}</th>" for c in header_cells)
1198
+ body_html = ""
1199
+ for row in data_rows:
1200
+ cells = parse_row(row)
1201
+ body_html += "<tr>" + "".join(f"<td>{_inline_markdown(c)}</td>" for c in cells) + "</tr>"
1202
+
1203
+ return f"<table><thead><tr>{header_html}</tr></thead><tbody>{body_html}</tbody></table>"