Monorepo for Aesthetic.Computer aesthetic.computer
at main 1064 lines 30 kB view raw
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 &rarr;</a></p> 956 957 <h3>tools</h3> 958 ${docs.mcp.tools.map(tool => ` 959 <p><strong><code>${tool.name}</code></strong> &mdash; ${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> &mdash; ${resource.description}</p> 967 `).join('')} 968 969 <h3>prompts</h3> 970 ${docs.mcp.prompts.map(prompt => ` 971 <p><strong><code>${prompt.name}</code></strong> &mdash; ${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, '&amp;') 1060 .replace(/</g, '&lt;') 1061 .replace(/>/g, '&gt;') 1062 .replace(/"/g, '&quot;') 1063 .replace(/'/g, '&#039;'); 1064}