Monorepo for Aesthetic.Computer
aesthetic.computer
1// API Documentation
2// Returns LLM-friendly documentation of public aesthetic.computer APIs
3
4import { respond } from "../../backend/http.mjs";
5
6export async function handler(event, context) {
7 const apiDocs = {
8 title: "aesthetic.computer Public API",
9 version: "1.0",
10 description: "Public APIs for publishing creative works anonymously to aesthetic.computer",
11 baseURL: "https://aesthetic.computer",
12
13 mcp: {
14 title: "MCP Server",
15 description: "Model Context Protocol server for AI assistants (Claude, GPT-4, etc.) to interact with aesthetic.computer APIs",
16 package: "@aesthetic.computer/mcp",
17 install: "npx @aesthetic.computer/mcp",
18 repository: "https://tangled.org/aesthetic.computer/core/tree/main/mcp-server",
19
20 tools: [
21 {
22 name: "publish_piece",
23 description: "Publish a JavaScript piece to aesthetic.computer",
24 input: { source: "string", name: "string (optional)" },
25 output: { code: "string", url: "string", cached: "boolean" }
26 },
27 {
28 name: "publish_kidlisp",
29 description: "Publish KidLisp code to aesthetic.computer",
30 input: { source: "string" },
31 output: { code: "string", url: "string", cached: "boolean" }
32 },
33 {
34 name: "publish_clock",
35 description: "Publish a clock melody to aesthetic.computer",
36 input: { source: "string" },
37 output: { code: "string", url: "string", cached: "boolean" }
38 },
39 {
40 name: "get_api_info",
41 description: "Fetch the full API documentation",
42 input: {},
43 output: "API documentation object"
44 }
45 ],
46
47 resources: [
48 {
49 uri: "aesthetic-computer://piece-template",
50 description: "Starter template for a new aesthetic.computer piece with all lifecycle functions"
51 },
52 {
53 uri: "aesthetic-computer://kidlisp-reference",
54 description: "Quick reference guide for KidLisp syntax and common functions"
55 }
56 ],
57
58 prompts: [
59 {
60 name: "create-piece",
61 description: "Guided prompt for creating an aesthetic.computer piece",
62 arguments: ["name (required)", "description (required)"]
63 }
64 ],
65
66 configuration: {
67 "Claude Desktop": `{
68 "mcpServers": {
69 "aesthetic-computer": {
70 "command": "npx",
71 "args": ["-y", "@aesthetic.computer/mcp"],
72 "env": {
73 "AC_TOKEN": "optional-bearer-token"
74 }
75 }
76 }
77}`,
78 "Claude Code": `{
79 "mcpServers": {
80 "aesthetic-computer": {
81 "command": "npx",
82 "args": ["-y", "@aesthetic-computer/mcp"]
83 }
84 }
85}`,
86 "Cursor": `{
87 "mcpServers": {
88 "aesthetic-computer": {
89 "command": "npx",
90 "args": ["-y", "@aesthetic-computer/mcp"]
91 }
92 }
93}`
94 }
95 },
96
97 endpoints: [
98 {
99 name: "Store KidLisp Code",
100 method: "POST",
101 path: "/api/store-kidlisp",
102 description: "Publish KidLisp code anonymously and get a short URL for sharing",
103 authentication: "Optional (Bearer token for authenticated users)",
104 requestBody: {
105 contentType: "application/json",
106 schema: {
107 source: {
108 type: "string",
109 required: true,
110 description: "KidLisp source code (max 50,000 characters)",
111 example: "(wipe blue)\n(ink red)\n(box 10 10 50 50)"
112 }
113 }
114 },
115 responseBody: {
116 schema: {
117 code: {
118 type: "string",
119 description: "Short code for accessing the piece (e.g. 'abc123')"
120 },
121 cached: {
122 type: "boolean",
123 description: "True if code already existed (deduplication)"
124 }
125 }
126 },
127 examples: [
128 {
129 title: "Publish a KidLisp Piece",
130 description: "Create a simple animated piece with KidLisp",
131 curl: `curl -X POST https://aesthetic.computer/api/store-kidlisp \\
132 -H "Content-Type: application/json" \\
133 -d '{
134 "source": "(wipe blue)\\n(ink yellow)\\n(circle (/ w 2) (/ h 2) 100)"
135 }'`,
136 javascript: `const response = await fetch("https://aesthetic.computer/api/store-kidlisp", {
137 method: "POST",
138 headers: { "Content-Type": "application/json" },
139 body: JSON.stringify({
140 source: "(wipe blue)\\n(ink yellow)\\n(circle (/ w 2) (/ h 2) 100)"
141 })
142});
143
144const { code, cached } = await response.json();
145console.log(\`View at: https://aesthetic.computer/\${code}\`);`,
146 python: `import requests
147
148response = requests.post(
149 "https://aesthetic.computer/api/store-kidlisp",
150 json={
151 "source": "(wipe blue)\\n(ink yellow)\\n(circle (/ w 2) (/ h 2) 100)"
152 }
153)
154
155data = response.json()
156print(f"View at: https://aesthetic.computer/{data['code']}")`,
157 response: {
158 status: 201,
159 body: {
160 code: "xyz789",
161 cached: false
162 }
163 }
164 }
165 ]
166 },
167
168 {
169 name: "Store Clock Melody",
170 method: "POST",
171 path: "/api/store-clock",
172 description: "Publish a clock melody string and get a pronounceable short code",
173 authentication: "Optional (Bearer token for authenticated users)",
174 requestBody: {
175 contentType: "application/json",
176 schema: {
177 source: {
178 type: "string",
179 required: true,
180 description: "Clock melody string (max 10,000 characters)",
181 example: "c4 d4 e4 f4 g4"
182 },
183 melody: {
184 type: "string",
185 required: false,
186 description: "Legacy field name (use 'source' instead)"
187 }
188 }
189 },
190 responseBody: {
191 schema: {
192 code: {
193 type: "string",
194 description: "Pronounceable short code (e.g. 'bako', 'milu')"
195 },
196 cached: {
197 type: "boolean",
198 description: "True if melody already existed (deduplication)"
199 }
200 }
201 },
202 examples: [
203 {
204 title: "Publish a Clock Melody",
205 description: "Store a musical sequence for the clock piece",
206 curl: `curl -X POST https://aesthetic.computer/api/store-clock \\
207 -H "Content-Type: application/json" \\
208 -d '{
209 "source": "c4 e4 g4 c5 g4 e4 c4"
210 }'`,
211 javascript: `const response = await fetch("https://aesthetic.computer/api/store-clock", {
212 method: "POST",
213 headers: { "Content-Type": "application/json" },
214 body: JSON.stringify({
215 source: "c4 e4 g4 c5 g4 e4 c4"
216 })
217});
218
219const { code, cached } = await response.json();
220console.log(\`Listen at: https://aesthetic.computer/clock~\${code}\`);`,
221 python: `import requests
222
223response = requests.post(
224 "https://aesthetic.computer/api/store-clock",
225 json={
226 "source": "c4 e4 g4 c5 g4 e4 c4"
227 }
228)
229
230data = response.json()
231print(f"Listen at: https://aesthetic.computer/clock~{data['code']}")`,
232 response: {
233 status: 201,
234 body: {
235 code: "bako",
236 cached: false
237 }
238 }
239 }
240 ]
241 },
242
243 {
244 name: "Store JavaScript Piece",
245 method: "POST",
246 path: "/api/store-piece",
247 description: "Publish a JavaScript piece (.mjs) anonymously by providing source code as a string. No S3 credentials needed - the server handles storage automatically.",
248 authentication: "Optional (Bearer token for authenticated users)",
249 requestBody: {
250 contentType: "application/json",
251 schema: {
252 source: {
253 type: "string",
254 required: true,
255 description: "JavaScript piece source code (max 100,000 characters). Must contain at least one lifecycle function export.",
256 example: "export function boot($) { $.wipe('blue'); }\nexport function paint($) { $.ink('red'); $.box(10, 10, 50, 50); }"
257 },
258 name: {
259 type: "string",
260 required: false,
261 description: "Optional name for the piece (used for code generation)"
262 }
263 }
264 },
265 responseBody: {
266 schema: {
267 code: {
268 type: "string",
269 description: "Short code for accessing the piece (e.g. 'drift', 'wave')"
270 },
271 cached: {
272 type: "boolean",
273 description: "True if code already existed (deduplication)"
274 },
275 url: {
276 type: "string",
277 description: "Full URL to view the piece"
278 }
279 }
280 },
281 examples: [
282 {
283 title: "Publish a Simple Piece",
284 description: "Create a piece with basic drawing",
285 curl: `curl -X POST https://aesthetic.computer/api/store-piece \\
286 -H "Content-Type: application/json" \\
287 -d '{
288 "source": "export function boot($) {\\n $.wipe(\"blue\");\\n}\\n\\nexport function paint($) {\\n $.ink(\"red\");\\n $.box(10, 10, 50, 50);\\n}",
289 "name": "red-box"
290 }'`,
291 javascript: `const source = \`export function boot($) {
292 $.wipe("blue");
293}
294
295export function paint($) {
296 $.ink("red");
297 $.box(10, 10, 50, 50);
298}\`;
299
300const response = await fetch("https://aesthetic.computer/api/store-piece", {
301 method: "POST",
302 headers: { "Content-Type": "application/json" },
303 body: JSON.stringify({
304 source,
305 name: "red-box"
306 })
307});
308
309const { code, url, cached } = await response.json();
310console.log(\`View at: \${url}\`);`,
311 python: `import requests
312
313source = """export function boot($) {
314 $.wipe("blue");
315}
316
317export function paint($) {
318 $.ink("red");
319 $.box(10, 10, 50, 50);
320}"""
321
322response = requests.post(
323 "https://aesthetic.computer/api/store-piece",
324 json={
325 "source": source,
326 "name": "red-box"
327 }
328)
329
330data = response.json()
331print(f"View at: {data['url']}")`,
332 response: {
333 status: 201,
334 body: {
335 code: "red-box",
336 cached: false,
337 url: "https://aesthetic.computer/red-box"
338 }
339 }
340 },
341 {
342 title: "Publish Interactive Piece",
343 description: "Create a piece with user interaction",
344 curl: `curl -X POST https://aesthetic.computer/api/store-piece \\
345 -H "Content-Type: "application/json" \\
346 -d '{
347 "source": "let x = 0;\\n\\nexport function boot($) {\\n x = $.screen.width / 2;\\n}\\n\\nexport function paint($) {\\n $.wipe(\"black\");\\n $.ink(\"yellow\");\\n $.circle(x, $.screen.height / 2, 20);\\n}\\n\\nexport function act($) {\\n if ($.event.is(\"touch\")) x = $.event.x;\\n}"
348 }'`,
349 javascript: `const source = \`let x = 0;
350
351export function boot($) {
352 x = $.screen.width / 2;
353}
354
355export function paint($) {
356 $.wipe("black");
357 $.ink("yellow");
358 $.circle(x, $.screen.height / 2, 20);
359}
360
361export function act($) {
362 if ($.event.is("touch")) x = $.event.x;
363}\`;
364
365const response = await fetch("https://aesthetic.computer/api/store-piece", {
366 method: "POST",
367 headers: { "Content-Type": "application/json" },
368 body: JSON.stringify({ source })
369});
370
371const { code, url } = await response.json();
372console.log(\`View at: \${url}\`);`,
373 python: `import requests
374
375source = """let x = 0;
376
377export function boot($) {
378 x = $.screen.width / 2;
379}
380
381export function paint($) {
382 $.wipe("black");
383 $.ink("yellow");
384 $.circle(x, $.screen.height / 2, 20);
385}
386
387export function act($) {
388 if ($.event.is("touch")) x = $.event.x;
389}"""
390
391response = requests.post(
392 "https://aesthetic.computer/api/store-piece",
393 json={"source": source}
394)
395
396data = response.json()
397print(f"View at: {data['url']}")`,
398 response: {
399 status: 201,
400 body: {
401 code: "touch",
402 cached: false,
403 url: "https://aesthetic.computer/touch"
404 }
405 }
406 }
407 ]
408 },
409
410 {
411 name: "Track Media (Publish Artwork)",
412 method: "POST",
413 path: "/api/track-media",
414 description: "Publish a painting (PNG), JavaScript piece (MJS), or recording tape (ZIP) anonymously. Note: Files must be uploaded to S3/storage before calling this endpoint.",
415 authentication: "Optional (Bearer token for authenticated users)",
416 requestBody: {
417 contentType: "application/json",
418 schema: {
419 slug: {
420 type: "string",
421 required: true,
422 description: "S3/storage path where the file was uploaded"
423 },
424 ext: {
425 type: "string",
426 required: true,
427 enum: ["png", "mjs", "zip"],
428 description: "File extension: 'png' for paintings, 'mjs' for JavaScript pieces, 'zip' for tapes"
429 },
430 metadata: {
431 type: "object",
432 required: false,
433 description: "Optional metadata (for tapes: totalDuration in seconds, max 30s)",
434 properties: {
435 totalDuration: {
436 type: "number",
437 description: "Duration in seconds (tapes only, max 30)"
438 }
439 }
440 }
441 }
442 },
443 responseBody: {
444 schema: {
445 code: {
446 type: "string",
447 description: "Short code for accessing the media"
448 }
449 }
450 },
451 examples: [
452 {
453 title: "Publish a JavaScript Piece",
454 description: "After uploading .mjs file to S3, register it in the database",
455 curl: `curl -X POST https://aesthetic.computer/api/track-media \\
456 -H "Content-Type: application/json" \\
457 -d '{
458 "slug": "2026/02/12/my-piece.mjs",
459 "ext": "mjs"
460 }'`,
461 javascript: `// Step 1: Upload your .mjs file to S3 (requires credentials)
462// Step 2: Register the uploaded file
463const response = await fetch("https://aesthetic.computer/api/track-media", {
464 method: "POST",
465 headers: { "Content-Type": "application/json" },
466 body: JSON.stringify({
467 slug: "2026/02/12/my-piece.mjs",
468 ext: "mjs"
469 })
470});
471
472const { code } = await response.json();
473console.log(\`View at: https://aesthetic.computer/\${code}\`);`,
474 python: `import requests
475
476# After uploading your .mjs file to S3
477response = requests.post(
478 "https://aesthetic.computer/api/track-media",
479 json={
480 "slug": "2026/02/12/my-piece.mjs",
481 "ext": "mjs"
482 }
483)
484
485data = response.json()
486print(f"View at: https://aesthetic.computer/{data['code']}")`,
487 response: {
488 status: 200,
489 body: {
490 code: "abc456"
491 }
492 }
493 },
494 {
495 title: "Publish a Painting (PNG)",
496 description: "Register a painting image after uploading to S3",
497 curl: `curl -X POST https://aesthetic.computer/api/track-media \\
498 -H "Content-Type: application/json" \\
499 -d '{
500 "slug": "2026/02/12/my-painting.png",
501 "ext": "png"
502 }'`,
503 javascript: `const response = await fetch("https://aesthetic.computer/api/track-media", {
504 method: "POST",
505 headers: { "Content-Type": "application/json" },
506 body: JSON.stringify({
507 slug: "2026/02/12/my-painting.png",
508 ext: "png"
509 })
510});
511
512const { code } = await response.json();
513console.log(\`View at: https://aesthetic.computer/\${code}\`);`,
514 python: `import requests
515
516response = requests.post(
517 "https://aesthetic.computer/api/track-media",
518 json={
519 "slug": "2026/02/12/my-painting.png",
520 "ext": "png"
521 }
522)
523
524data = response.json()
525print(f"View at: https://aesthetic.computer/{data['code']}")`,
526 response: {
527 status: 200,
528 body: {
529 code: "def789"
530 }
531 }
532 },
533 {
534 title: "Publish a Recording Tape (ZIP)",
535 description: "Register a recording after uploading ZIP to S3",
536 curl: `curl -X POST https://aesthetic.computer/api/track-media \\
537 -H "Content-Type: application/json" \\
538 -d '{
539 "slug": "2026/02/12/my-recording.zip",
540 "ext": "zip",
541 "metadata": {
542 "totalDuration": 15.5
543 }
544 }'`,
545 javascript: `const response = await fetch("https://aesthetic.computer/api/track-media", {
546 method: "POST",
547 headers: { "Content-Type": "application/json" },
548 body: JSON.stringify({
549 slug: "2026/02/12/my-recording.zip",
550 ext: "zip",
551 metadata: {
552 totalDuration: 15.5 // seconds (max 30)
553 }
554 })
555});
556
557const { code } = await response.json();
558console.log(\`Watch at: https://aesthetic.computer/\${code}\`);`,
559 python: `import requests
560
561response = requests.post(
562 "https://aesthetic.computer/api/track-media",
563 json={
564 "slug": "2026/02/12/my-recording.zip",
565 "ext": "zip",
566 "metadata": {
567 "totalDuration": 15.5 # seconds (max 30)
568 }
569 }
570)
571
572data = response.json()
573print(f"Watch at: https://aesthetic.computer/{data['code']}")`,
574 response: {
575 status: 200,
576 body: {
577 code: "ghi012"
578 }
579 }
580 }
581 ]
582 }
583 ],
584
585 notes: [
586 "✨ All endpoints support anonymous (guest) publishing without authentication",
587 "🔑 To associate uploads with your account, include a Bearer token in the Authorization header",
588 "🎨 KidLisp is a creative coding language - visit https://kidlisp.com for documentation",
589 "💾 /api/store-piece handles storage automatically - no S3 credentials needed",
590 "📦 For /api/track-media: Files must be uploaded to S3/storage first (contact admins for credentials)",
591 "📏 Maximum source code lengths: KidLisp 50,000 chars, JavaScript pieces 100,000 chars",
592 "⏱️ Maximum clock melody length: 10,000 characters",
593 "🎬 Maximum tape duration: 30 seconds",
594 "♻️ Duplicate content is automatically deduplicated (same content returns same code)",
595 "🔧 JavaScript pieces must export at least one lifecycle function: boot, paint, sim, or act"
596 ],
597
598 relatedResources: [
599 {
600 name: "KidLisp Documentation",
601 url: "https://kidlisp.com"
602 },
603 {
604 name: "aesthetic.computer Main Site",
605 url: "https://aesthetic.computer"
606 }
607 ]
608 };
609
610 // Content negotiation: HTML for browsers, JSON for APIs/LLMs
611 const acceptHeader = event.headers?.accept || "";
612 const format = event.queryStringParameters?.format;
613
614 // Explicit format parameter takes precedence
615 const wantsHTML = format === "html" ||
616 (!format && acceptHeader.includes("text/html"));
617
618 if (wantsHTML) {
619 // Serve HTML documentation for browsers
620 const html = generateHTML(apiDocs);
621 return respond(200, html, {
622 "Content-Type": "text/html",
623 "Access-Control-Allow-Origin": "*"
624 });
625 }
626
627 // Default: Return as pretty-printed JSON for LLMs/APIs
628 return respond(200, apiDocs, {
629 "Content-Type": "application/json",
630 "Access-Control-Allow-Origin": "*"
631 });
632}
633
634function generateHTML(docs) {
635 return `<!DOCTYPE html>
636<html lang="en">
637<head>
638 <meta charset="UTF-8">
639 <meta name="viewport" content="width=device-width, initial-scale=1.0">
640 <title>${docs.title}</title>
641 <link rel="icon" href="https://aesthetic.computer/favicon.ico" type="image/x-icon">
642 <style>
643 /* CSS Variables for theming */
644 :root {
645 --bg: #f7f7f7;
646 --bg-alt: #fff;
647 --text: #111;
648 --text-dim: #666;
649 --border: #ddd;
650 --accent: rgb(205, 92, 155);
651 --accent-hover: rgb(240, 180, 215);
652 --code-bg: #e8e8e8;
653 --code-text: #a31515;
654 --pre-bg: #2a2520;
655 --pre-text: #fffacd;
656 --link: rgb(0, 80, 180);
657 --link-hover: rgb(205, 92, 155);
658 }
659
660 @media (prefers-color-scheme: dark) {
661 :root {
662 --bg: #1e1e1e;
663 --bg-alt: #252526;
664 --text: #d4d4d4;
665 --text-dim: #858585;
666 --border: #3e3e42;
667 --accent: rgb(205, 92, 155);
668 --accent-hover: rgb(240, 180, 215);
669 --code-bg: #252526;
670 --code-text: #ce9178;
671 --pre-bg: #1a1a1a;
672 --pre-text: #e2e8f0;
673 --link: rgb(150, 180, 255);
674 --link-hover: rgb(205, 92, 155);
675 }
676 }
677
678 * {
679 box-sizing: border-box;
680 margin: 0;
681 padding: 0;
682 }
683
684 body {
685 font-family: 'Berkeley Mono Variable', 'Noto Sans Mono', 'SF Mono', Monaco, Consolas, monospace;
686 background: var(--bg);
687 color: var(--text);
688 line-height: 1.6;
689 padding: 1em;
690 max-width: 960px;
691 margin: 0 auto;
692 font-size: 14px;
693 }
694
695 .header {
696 border: 2px solid var(--border);
697 padding: 1.5em;
698 margin-bottom: 1em;
699 background: var(--bg-alt);
700 }
701
702 .ascii-art {
703 font-size: 10px;
704 line-height: 1.2;
705 color: var(--text-dim);
706 margin-bottom: 1em;
707 white-space: pre;
708 font-family: monospace;
709 }
710
711 h1 {
712 font-size: 1.5em;
713 font-weight: bold;
714 color: var(--text);
715 margin-bottom: 0.5em;
716 }
717
718 .tagline {
719 color: var(--text-dim);
720 margin-bottom: 1em;
721 }
722
723 .version {
724 display: inline-block;
725 border: 1px solid var(--border);
726 color: var(--text-dim);
727 padding: 0.2em 0.5em;
728 font-size: 0.85em;
729 margin-bottom: 1em;
730 }
731
732 .quick-links {
733 display: flex;
734 gap: 0.5em;
735 flex-wrap: wrap;
736 margin-top: 1em;
737 }
738
739 .btn {
740 display: inline-block;
741 padding: 0.5em 1em;
742 background: var(--accent);
743 color: white;
744 text-decoration: none;
745 border: 2px solid var(--accent);
746 transition: all 0.2s;
747 }
748
749 .btn:hover {
750 background: var(--accent-hover);
751 border-color: var(--accent-hover);
752 color: var(--text);
753 }
754
755 .btn-secondary {
756 background: transparent;
757 color: var(--accent);
758 }
759
760 .section {
761 border: 2px solid var(--border);
762 padding: 1.5em;
763 margin-bottom: 1em;
764 background: var(--bg-alt);
765 }
766
767 h2 {
768 font-size: 1.3em;
769 font-weight: bold;
770 color: var(--text);
771 margin-bottom: 1em;
772 border-bottom: 2px solid var(--border);
773 padding-bottom: 0.5em;
774 }
775
776 h3 {
777 font-size: 1.1em;
778 font-weight: bold;
779 margin-top: 1.5em;
780 margin-bottom: 0.5em;
781 }
782
783 h4 {
784 font-size: 1em;
785 font-weight: bold;
786 margin-top: 1em;
787 margin-bottom: 0.5em;
788 color: var(--text-dim);
789 }
790
791 p {
792 margin-bottom: 0.8em;
793 }
794
795 code {
796 font-family: 'Berkeley Mono Variable', 'Noto Sans Mono', Monaco, Consolas, monospace;
797 background: var(--code-bg);
798 padding: 0.2em 0.4em;
799 font-size: 0.95em;
800 color: var(--code-text);
801 }
802
803 pre {
804 background: var(--pre-bg);
805 color: var(--pre-text);
806 padding: 1em;
807 overflow-x: auto;
808 margin: 1em 0;
809 border: 1px solid var(--border);
810 line-height: 1.4;
811 }
812
813 pre code {
814 background: transparent;
815 padding: 0;
816 color: var(--pre-text);
817 }
818
819 .method {
820 display: inline-block;
821 background: var(--accent);
822 color: white;
823 padding: 0.2em 0.5em;
824 font-weight: bold;
825 font-size: 0.85em;
826 margin-right: 0.5em;
827 }
828
829 .path {
830 font-weight: bold;
831 }
832
833 .tabs {
834 display: flex;
835 gap: 0;
836 margin: 1em 0 0 0;
837 border-bottom: 2px solid var(--border);
838 }
839
840 .tab {
841 padding: 0.5em 1em;
842 background: transparent;
843 border: none;
844 cursor: pointer;
845 font-family: inherit;
846 font-size: 0.95em;
847 color: var(--text-dim);
848 border-bottom: 2px solid transparent;
849 margin-bottom: -2px;
850 }
851
852 .tab:hover {
853 color: var(--accent);
854 background: var(--code-bg);
855 }
856
857 .tab.active {
858 color: var(--accent);
859 border-bottom-color: var(--accent);
860 }
861
862 .tab-content {
863 display: none;
864 }
865
866 .tab-content.active {
867 display: block;
868 }
869
870 .notes {
871 border: 2px solid var(--border);
872 padding: 1em;
873 margin: 1em 0;
874 background: var(--bg-alt);
875 }
876
877 .notes h3 {
878 color: var(--text);
879 margin-bottom: 0.5em;
880 }
881
882 .notes ul {
883 margin-left: 1.5em;
884 list-style: square;
885 }
886
887 .notes li {
888 margin-bottom: 0.5em;
889 }
890
891 .footer {
892 margin-top: 2em;
893 padding-top: 1em;
894 border-top: 1px solid var(--border);
895 color: var(--text-dim);
896 font-size: 0.9em;
897 }
898
899 .footer a {
900 color: var(--link);
901 text-decoration: none;
902 }
903
904 .footer a:hover {
905 color: var(--link-hover);
906 text-decoration: underline;
907 }
908
909 a {
910 color: var(--link);
911 text-decoration: none;
912 }
913
914 a:hover {
915 color: var(--link-hover);
916 text-decoration: underline;
917 }
918
919 .endpoint-card {
920 border-left: 3px solid var(--border);
921 padding-left: 1em;
922 margin: 2em 0;
923 }
924
925 @media (max-width: 768px) {
926 body { padding: 0.5em; font-size: 13px; }
927 .header { padding: 1em; }
928 .section { padding: 1em; }
929 .ascii-art { font-size: 8px; }
930 }
931 </style>
932</head>
933<body>
934 <div class="header">
935 <div class="ascii-art"> ___ ____ ____
936/ __)( _ \\(_ _)
937\\__ \\ ) __/ _)(_
938(___/(__) (____)</div>
939 <h1>${docs.title}</h1>
940 <p class="tagline">${docs.description}</p>
941 <div class="version">version ${docs.version}</div>
942 <div class="quick-links">
943 <a href="?format=json" class="btn btn-secondary">[ view json ]</a>
944 <a href="https://aesthetic.computer" class="btn">[ home ]</a>
945 <a href="https://tangled.org/aesthetic.computer/core" class="btn">[ tangled ]</a>
946 </div>
947 </div>
948
949 <div class="section">
950 <h2>// MCP Server</h2>
951 <p><strong>${docs.mcp.title}</strong></p>
952 <p>${docs.mcp.description}</p>
953 <p><strong>package:</strong> <code>${docs.mcp.package}</code></p>
954 <p><strong>install:</strong> <code>${docs.mcp.install}</code></p>
955 <p><a href="${docs.mcp.repository}">view on tangled →</a></p>
956
957 <h3>tools</h3>
958 ${docs.mcp.tools.map(tool => `
959 <p><strong><code>${tool.name}</code></strong> — ${tool.description}</p>
960 <p>input: <code>${JSON.stringify(tool.input)}</code><br>
961 output: <code>${typeof tool.output === 'string' ? tool.output : JSON.stringify(tool.output)}</code></p>
962 `).join('')}
963
964 <h3>resources</h3>
965 ${docs.mcp.resources.map(resource => `
966 <p><strong><code>${resource.uri}</code></strong> — ${resource.description}</p>
967 `).join('')}
968
969 <h3>prompts</h3>
970 ${docs.mcp.prompts.map(prompt => `
971 <p><strong><code>${prompt.name}</code></strong> — ${prompt.description}</p>
972 <p>args: ${prompt.arguments.join(', ')}</p>
973 `).join('')}
974
975 <h3>configuration examples</h3>
976 ${Object.entries(docs.mcp.configuration).map(([client, config]) => `
977 <h4>${client}</h4>
978 <pre><code>${escapeHTML(config)}</code></pre>
979 `).join('')}
980 </div>
981
982 <div class="section">
983 <h2>// HTTP endpoints</h2>
984
985 ${docs.endpoints.map((endpoint, idx) => `
986 <div class="endpoint-card">
987 <h3><span class="method">${endpoint.method}</span> ${endpoint.name}</h3>
988 <p><code class="path">${docs.baseURL}${endpoint.path}</code></p>
989 <p>${endpoint.description}</p>
990
991 ${endpoint.examples.map((example, exIdx) => `
992 <h3>${example.title}</h3>
993 <p>${example.description}</p>
994
995 <div class="tabs" id="tabs-${idx}-${exIdx}">
996 <button class="tab active" onclick="showTab(${idx}, ${exIdx}, 'curl')">curl</button>
997 <button class="tab" onclick="showTab(${idx}, ${exIdx}, 'js')">javascript</button>
998 <button class="tab" onclick="showTab(${idx}, ${exIdx}, 'py')">python</button>
999 </div>
1000
1001 <div class="tab-content active" id="content-${idx}-${exIdx}-curl">
1002 <pre>${escapeHTML(example.curl)}</pre>
1003 </div>
1004 <div class="tab-content" id="content-${idx}-${exIdx}-js">
1005 <pre>${escapeHTML(example.javascript)}</pre>
1006 </div>
1007 <div class="tab-content" id="content-${idx}-${exIdx}-py">
1008 <pre>${escapeHTML(example.python)}</pre>
1009 </div>
1010
1011 <h4>response:</h4>
1012 <pre><code>${JSON.stringify(example.response.body, null, 2)}</code></pre>
1013 `).join('')}
1014 </div>
1015 `).join('')}
1016 </div>
1017
1018 <div class="notes">
1019 <h3>// important notes</h3>
1020 <ul>
1021 ${docs.notes.map(note => `<li>${note}</li>`).join('')}
1022 </ul>
1023 </div>
1024
1025 <div class="footer">
1026 ${docs.relatedResources.map(r =>
1027 `<a href="${r.url}" target="_blank">${r.name}</a>`
1028 ).join(' • ')}
1029 </div>
1030
1031 <script>
1032 function showTab(endpointIdx, exampleIdx, lang) {
1033 const tabsContainer = document.getElementById(\`tabs-\${endpointIdx}-\${exampleIdx}\`);
1034 const tabs = tabsContainer.querySelectorAll('.tab');
1035 const contents = ['curl', 'js', 'py'];
1036
1037 tabs.forEach((tab, i) => {
1038 tab.classList.remove('active');
1039 const contentId = \`content-\${endpointIdx}-\${exampleIdx}-\${contents[i]}\`;
1040 const content = document.getElementById(contentId);
1041 if (content) content.classList.remove('active');
1042 });
1043
1044 const activeTab = Array.from(tabs).find(t =>
1045 t.textContent.toLowerCase().includes(lang === 'js' ? 'javascript' : lang)
1046 );
1047 if (activeTab) activeTab.classList.add('active');
1048
1049 const activeContent = document.getElementById(\`content-\${endpointIdx}-\${exampleIdx}-\${lang}\`);
1050 if (activeContent) activeContent.classList.add('active');
1051 }
1052 </script>
1053</body>
1054</html>`;
1055}
1056
1057function escapeHTML(str) {
1058 return str
1059 .replace(/&/g, '&')
1060 .replace(/</g, '<')
1061 .replace(/>/g, '>')
1062 .replace(/"/g, '"')
1063 .replace(/'/g, ''');
1064}