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.
- contree_mcp/__init__.py +0 -0
- contree_mcp/__main__.py +25 -0
- contree_mcp/app.py +240 -0
- contree_mcp/arguments.py +35 -0
- contree_mcp/auth/__init__.py +2 -0
- contree_mcp/auth/registry.py +236 -0
- contree_mcp/backend_types.py +301 -0
- contree_mcp/cache.py +208 -0
- contree_mcp/client.py +711 -0
- contree_mcp/context.py +53 -0
- contree_mcp/docs.py +1203 -0
- contree_mcp/file_cache.py +381 -0
- contree_mcp/prompts.py +238 -0
- contree_mcp/py.typed +0 -0
- contree_mcp/resources/__init__.py +17 -0
- contree_mcp/resources/guide.py +715 -0
- contree_mcp/resources/image_lineage.py +46 -0
- contree_mcp/resources/image_ls.py +32 -0
- contree_mcp/resources/import_operation.py +52 -0
- contree_mcp/resources/instance_operation.py +52 -0
- contree_mcp/resources/read_file.py +33 -0
- contree_mcp/resources/static.py +12 -0
- contree_mcp/server.py +77 -0
- contree_mcp/tools/__init__.py +39 -0
- contree_mcp/tools/cancel_operation.py +36 -0
- contree_mcp/tools/download.py +128 -0
- contree_mcp/tools/get_guide.py +54 -0
- contree_mcp/tools/get_image.py +30 -0
- contree_mcp/tools/get_operation.py +26 -0
- contree_mcp/tools/import_image.py +99 -0
- contree_mcp/tools/list_files.py +80 -0
- contree_mcp/tools/list_images.py +50 -0
- contree_mcp/tools/list_operations.py +46 -0
- contree_mcp/tools/read_file.py +47 -0
- contree_mcp/tools/registry_auth.py +71 -0
- contree_mcp/tools/registry_token_obtain.py +80 -0
- contree_mcp/tools/rsync.py +46 -0
- contree_mcp/tools/run.py +97 -0
- contree_mcp/tools/set_tag.py +31 -0
- contree_mcp/tools/upload.py +50 -0
- contree_mcp/tools/wait_operations.py +79 -0
- contree_mcp-0.1.0.dist-info/METADATA +450 -0
- contree_mcp-0.1.0.dist-info/RECORD +46 -0
- contree_mcp-0.1.0.dist-info/WHEEL +4 -0
- contree_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- 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 > ~/.config/contree/mcp.ini << '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 — 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 & 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">▾</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>"
|