Spaces:
Running
Running
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| background: #fafafa; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 100vw; | |
| height: 100vh; | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| padding: 0; | |
| overflow: hidden; | |
| } | |
| .canvas-root { | |
| display: inline-flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 24px 20px; | |
| } | |
| /* Process boundary container */ | |
| .process-boundary { | |
| border: 2px dashed #9e9e9e; | |
| border-radius: 20px; | |
| padding: 30px 30px; | |
| background: rgba(255, 255, 255, 0.5); | |
| position: relative; | |
| } | |
| .process-label { | |
| position: absolute; | |
| top: -14px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: #fafafa; | |
| padding: 4px 20px; | |
| font-size: 16px; | |
| font-weight: 600; | |
| color: #616161; | |
| white-space: nowrap; | |
| } | |
| .diagram { | |
| position: relative; | |
| width: 500px; | |
| height: 220px; | |
| } | |
| /* Entity base styles */ | |
| .entity { | |
| position: absolute; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| /* Client - starts grey, becomes blue */ | |
| .client { left: 0; top: 30px; } | |
| .client .entity-box { | |
| width: 130px; | |
| min-height: 75px; | |
| height: 80px; | |
| border-radius: 16px; | |
| background: #f5f5f5; | |
| border: 3px solid #9e9e9e; | |
| color: #9e9e9e; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: 600; | |
| font-size: 14px; | |
| transition: all 0.5s ease; | |
| text-align: center; | |
| line-height: 1.3; | |
| gap: 4px; | |
| } | |
| .client .entity-box.alive { | |
| background: #e3f2fd; | |
| border-color: #1976d2; | |
| color: #1976d2; | |
| } | |
| .client .entity-box.active { | |
| background: #bbdefb; | |
| border-color: #1976d2; | |
| color: #1976d2; | |
| box-shadow: 0 4px 20px rgba(25, 118, 210, 0.3); | |
| } | |
| /* Server - starts grey, becomes orange */ | |
| .server { right: 0; top: 30px; } | |
| .server .entity-box { | |
| width: 130px; | |
| min-height: 75px; | |
| height: 80px; | |
| border-radius: 16px; | |
| background: #f5f5f5; | |
| border: 3px solid #9e9e9e; | |
| color: #9e9e9e; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: 600; | |
| font-size: 14px; | |
| transition: all 0.5s ease; | |
| text-align: center; | |
| line-height: 1.3; | |
| gap: 4px; | |
| } | |
| .server .entity-box.alive { | |
| background: #fff3e0; | |
| border-color: #ef6c00; | |
| color: #ef6c00; | |
| } | |
| .server .entity-box.active { | |
| background: #ffe0b2; | |
| border-color: #ef6c00; | |
| color: #ef6c00; | |
| box-shadow: 0 4px 20px rgba(239, 108, 0, 0.3); | |
| } | |
| .server .entity-box.waiting { | |
| animation: waitingPulse 1s ease-in-out infinite; | |
| } | |
| @keyframes waitingPulse { | |
| 0%, 100% { | |
| background: #fff3e0; | |
| border-color: #ef6c00; | |
| box-shadow: 0 4px 20px rgba(239, 108, 0, 0.2); | |
| } | |
| 50% { | |
| background: #ffe0b2; | |
| border-color: #e65100; | |
| box-shadow: 0 4px 30px rgba(239, 108, 0, 0.5); | |
| } | |
| } | |
| .entity-status { | |
| font-size: 10px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| opacity: 0; | |
| transition: opacity 0.3s ease; | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| } | |
| .entity-status.visible { | |
| opacity: 1; | |
| } | |
| .entity-status.waiting { | |
| background: #e65100; | |
| color: white; | |
| animation: statusBlink 0.8s ease-in-out infinite; | |
| } | |
| @keyframes statusBlink { | |
| 0%, 100% { opacity: 1; transform: scale(1); } | |
| 50% { opacity: 0.7; transform: scale(1.05); } | |
| } | |
| /* State indicator badge - shows capabilities learned */ | |
| .state-badge { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 4px; | |
| padding: 8px 12px; | |
| border-radius: 10px; | |
| font-size: 11px; | |
| font-weight: 600; | |
| transition: all 0.4s ease; | |
| width: 130px; | |
| min-height: 75px; | |
| /* Default: gray/inactive state */ | |
| background: #f5f5f5; | |
| border: 1px solid #e0e0e0; | |
| color: #9e9e9e; | |
| } | |
| .state-badge.active { | |
| background: #e8f5e9; | |
| border: 1px solid #a5d6a7; | |
| color: #2e7d32; | |
| } | |
| .state-badge .badge-title { | |
| font-size: 9px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| color: #999; | |
| font-weight: 500; | |
| } | |
| .state-badge.active .badge-title { | |
| color: #666; | |
| } | |
| .state-badge .caps-list { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 4px; | |
| justify-content: center; | |
| } | |
| .state-badge .cap { | |
| display: flex; | |
| align-items: center; | |
| gap: 3px; | |
| font-family: 'SF Mono', Monaco, monospace; | |
| font-size: 9px; | |
| background: white; | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| border: 1px solid #e0e0e0; | |
| opacity: 0; | |
| transition: opacity 0.3s ease; | |
| } | |
| .state-badge.active .cap { | |
| border-color: #c8e6c9; | |
| opacity: 1; | |
| } | |
| .state-badge .cap .check { | |
| color: #4caf50; | |
| font-weight: bold; | |
| } | |
| .state-badge .empty-state { | |
| font-size: 9px; | |
| color: #bdbdbd; | |
| font-style: italic; | |
| } | |
| .state-badge.active .empty-state { | |
| display: none; | |
| } | |
| /* Connection line */ | |
| .connection { | |
| position: absolute; | |
| left: 145px; | |
| right: 145px; | |
| top: 70px; | |
| height: 2px; | |
| background: #bdbdbd; | |
| } | |
| .connection-label { | |
| position: absolute; | |
| left: 50%; | |
| top: 50%; | |
| transform: translate(-50%, -50%); | |
| background: rgba(255, 255, 255, 0.9); | |
| padding: 4px 12px; | |
| font-size: 11px; | |
| color: #757575; | |
| font-weight: 500; | |
| white-space: nowrap; | |
| border-radius: 4px; | |
| } | |
| /* The moving message - centered around arrow */ | |
| .message { | |
| position: absolute; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 4px; | |
| opacity: 0; | |
| transition: opacity 0.2s ease; | |
| pointer-events: none; | |
| z-index: 100; | |
| transform: translateX(-50%); | |
| } | |
| .message.visible { | |
| opacity: 1; | |
| } | |
| .message-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .message-arrow { | |
| font-size: 28px; | |
| font-weight: bold; | |
| } | |
| .message-type { | |
| padding: 6px 12px; | |
| border-radius: 8px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| white-space: nowrap; | |
| box-shadow: 0 3px 12px rgba(0,0,0,0.15); | |
| } | |
| .message-detail { | |
| padding: 3px 8px; | |
| border-radius: 4px; | |
| font-size: 10px; | |
| font-family: 'SF Mono', Monaco, monospace; | |
| background: rgba(255,255,255,0.95); | |
| color: #666; | |
| border: 1px solid #e0e0e0; | |
| } | |
| .message.request .message-arrow { color: #1976d2; } | |
| .message.request .message-type { | |
| background: #1976d2; | |
| color: white; | |
| } | |
| .message.response .message-arrow { color: #ef6c00; } | |
| .message.response .message-type { | |
| background: #ef6c00; | |
| color: white; | |
| } | |
| .message.notification .message-arrow { color: #7b1fa2; } | |
| .message.notification .message-type { | |
| background: #7b1fa2; | |
| color: white; | |
| } | |
| .message.server-request .message-arrow { color: #ef6c00; } | |
| .message.server-request .message-type { | |
| background: #ef6c00; | |
| color: white; | |
| } | |
| .message.client-response .message-arrow { color: #1976d2; } | |
| .message.client-response .message-type { | |
| background: #1976d2; | |
| color: white; | |
| } | |
| /* Narration */ | |
| .narration { | |
| margin-top: 30px; | |
| padding: 16px 24px; | |
| background: white; | |
| border: 2px solid #e0e0e0; | |
| border-radius: 12px; | |
| font-size: 15px; | |
| color: #333; | |
| max-width: 650px; | |
| text-align: center; | |
| min-height: 56px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .narration .step-num { | |
| display: inline-block; | |
| width: 28px; | |
| height: 28px; | |
| background: #9e9e9e; | |
| color: white; | |
| border-radius: 50%; | |
| font-size: 14px; | |
| font-weight: 600; | |
| line-height: 28px; | |
| text-align: center; | |
| margin-right: 14px; | |
| flex-shrink: 0; | |
| transition: background 0.3s ease; | |
| } | |
| .narration.request-state .step-num { background: #1976d2; } | |
| .narration.response-state .step-num { background: #ef6c00; } | |
| .narration.notification-state .step-num { background: #7b1fa2; } | |
| .narration.success-state .step-num { background: #4caf50; } | |
| .narration.waiting-state .step-num { background: #e65100; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="canvas-root"> | |
| <div class="process-boundary"> | |
| <div class="process-label">STDIO (Same Process)</div> | |
| <div class="diagram"> | |
| <!-- Client --> | |
| <div class="entity client"> | |
| <div class="entity-box" id="client-box"> | |
| <span>MCP Client</span> | |
| <span class="entity-status" id="client-status"></span> | |
| </div> | |
| <div class="state-badge" id="client-state"> | |
| <div class="badge-title">Connection State</div> | |
| <div class="empty-state">not connected</div> | |
| <div class="caps-list"> | |
| <span class="cap"><span class="check">β</span> tools</span> | |
| <span class="cap"><span class="check">β</span> prompts</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Connection line --> | |
| <div class="connection"> | |
| <div class="connection-label">stdin / stdout</div> | |
| </div> | |
| <!-- Server --> | |
| <div class="entity server"> | |
| <div class="entity-box" id="server-box"> | |
| <span>MCP Server</span> | |
| <span class="entity-status" id="server-status"></span> | |
| </div> | |
| <div class="state-badge" id="server-state"> | |
| <div class="badge-title">Connection State</div> | |
| <div class="empty-state">not connected</div> | |
| <div class="caps-list"> | |
| <span class="cap"><span class="check">β</span> sampling</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Message --> | |
| <div class="message" id="message"> | |
| <div class="message-header"> | |
| <span class="message-type" id="message-type">initialize</span> | |
| <span class="message-arrow" id="message-arrow">β</span> | |
| </div> | |
| <div class="message-detail" id="message-detail">request</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="narration" id="narration"> | |
| <span class="step-num" id="step-num">β</span> | |
| <span id="narration-text">Client spawns server as subprocess...</span> | |
| </div> | |
| </div> | |
| <script src="shared/animation-config.js"></script> | |
| <script src="shared/diagram-helpers.js"></script> | |
| <script src="shared/canvas-scale.js"></script> | |
| <script> | |
| const message = document.getElementById('message'); | |
| const messageType = document.getElementById('message-type'); | |
| const messageArrow = document.getElementById('message-arrow'); | |
| const messageDetail = document.getElementById('message-detail'); | |
| const narration = document.getElementById('narration'); | |
| const narrationText = document.getElementById('narration-text'); | |
| const stepNum = document.getElementById('step-num'); | |
| const clientBox = document.getElementById('client-box'); | |
| const serverBox = document.getElementById('server-box'); | |
| const clientState = document.getElementById('client-state'); | |
| const serverState = document.getElementById('server-state'); | |
| const clientStatus = document.getElementById('client-status'); | |
| const serverStatus = document.getElementById('server-status'); | |
| const messageElements = { | |
| container: message, | |
| header: message.querySelector('.message-header'), | |
| type: messageType, | |
| arrow: messageArrow, | |
| detail: messageDetail, | |
| }; | |
| const clientCenter = 65; | |
| const serverCenter = 435; | |
| const msgY = 45; | |
| function setMessagePos(x, y) { | |
| setMessagePosition(message, x, y); | |
| } | |
| function animateMessage(fromX, fromY, toX, toY, duration, callback) { | |
| animateMessageBetween(message, fromX, fromY, toX, toY, duration, callback); | |
| } | |
| function configMessage(type, arrow, method, detail, direction) { | |
| configureMessage(messageElements, { | |
| type, | |
| arrow, | |
| method, | |
| detail, | |
| direction, | |
| }); | |
| } | |
| function setStatus(element, text, className) { | |
| element.textContent = text; | |
| element.className = 'entity-status' + (text ? ' visible' : '') + (className ? ' ' + className : ''); | |
| } | |
| const steps = [ | |
| // Step 0: Initial grey state | |
| { | |
| setup: () => { | |
| message.className = 'message'; | |
| stepNum.textContent = 'β'; | |
| narrationText.textContent = 'Client spawns server as subprocess...'; | |
| narration.className = 'narration'; | |
| clientBox.className = 'entity-box'; | |
| serverBox.className = 'entity-box'; | |
| clientState.classList.remove('active'); | |
| serverState.classList.remove('active'); | |
| setStatus(clientStatus, '', ''); | |
| setStatus(serverStatus, '', ''); | |
| }, | |
| animate: (done) => setTimeout(done, ANIMATION.INITIAL_PAUSE), | |
| after: () => {} | |
| }, | |
| // Step 1: Initialize request Client β Server | |
| { | |
| setup: () => { | |
| configMessage('request', 'β', 'initialize', 'InitializeRequest', 'right'); | |
| setMessagePos(clientCenter, msgY); | |
| stepNum.textContent = '1'; | |
| narrationText.textContent = 'Client sends InitializeRequest with capabilities'; | |
| narration.className = 'narration request-state'; | |
| clientBox.classList.add('alive', 'active'); | |
| }, | |
| animate: (done) => animateMessage(clientCenter, msgY, serverCenter, msgY, ANIMATION.MSG_DURATION, done), | |
| after: () => { | |
| clientBox.classList.remove('active'); | |
| serverBox.classList.add('alive', 'active'); | |
| } | |
| }, | |
| // Step 2: Initialize result Server β Client | |
| { | |
| setup: () => { | |
| configMessage('response', 'β', 'initialize', 'InitializeResult', 'left'); | |
| setMessagePos(serverCenter, msgY); | |
| stepNum.textContent = '2'; | |
| narrationText.textContent = 'Server responds with InitializeResult'; | |
| narration.className = 'narration response-state'; | |
| }, | |
| animate: (done) => animateMessage(serverCenter, msgY, clientCenter, msgY, ANIMATION.MSG_DURATION, done), | |
| after: () => { | |
| serverBox.classList.remove('active'); | |
| clientBox.classList.add('active'); | |
| } | |
| }, | |
| // Step 3: notifications/initialized Client β Server | |
| { | |
| setup: () => { | |
| clientBox.classList.remove('active'); | |
| configMessage('notification', 'β', 'initialized', 'notification', 'right'); | |
| setMessagePos(clientCenter, msgY); | |
| stepNum.textContent = '3'; | |
| narrationText.textContent = 'Client sends initialized notification'; | |
| narration.className = 'narration notification-state'; | |
| clientBox.classList.add('active'); | |
| }, | |
| animate: (done) => animateMessage(clientCenter, msgY, serverCenter, msgY, ANIMATION.MSG_DURATION, done), | |
| after: () => { | |
| clientBox.classList.remove('active'); | |
| clientState.classList.add('active'); | |
| serverState.classList.add('active'); | |
| } | |
| }, | |
| // Step 4: tools/list request | |
| { | |
| setup: () => { | |
| configMessage('request', 'β', 'tools/list', 'ListToolsRequest', 'right'); | |
| setMessagePos(clientCenter, msgY); | |
| stepNum.textContent = '4'; | |
| narrationText.textContent = 'Client requests tool list'; | |
| narration.className = 'narration request-state'; | |
| clientBox.classList.add('active'); | |
| }, | |
| animate: (done) => animateMessage(clientCenter, msgY, serverCenter, msgY, ANIMATION.MSG_DURATION, done), | |
| after: () => { | |
| clientBox.classList.remove('active'); | |
| serverBox.classList.add('active'); | |
| } | |
| }, | |
| // Step 5: tools/list response | |
| { | |
| setup: () => { | |
| configMessage('response', 'β', 'tools/list', 'ListToolsResult', 'left'); | |
| setMessagePos(serverCenter, msgY); | |
| stepNum.textContent = '5'; | |
| narrationText.textContent = 'Server returns ListToolsResult'; | |
| narration.className = 'narration response-state'; | |
| }, | |
| animate: (done) => animateMessage(serverCenter, msgY, clientCenter, msgY, ANIMATION.MSG_DURATION, done), | |
| after: () => { | |
| serverBox.classList.remove('active'); | |
| } | |
| }, | |
| // Step 6: prompts/list request | |
| { | |
| setup: () => { | |
| configMessage('request', 'β', 'prompts/list', 'ListPromptsRequest', 'right'); | |
| setMessagePos(clientCenter, msgY); | |
| stepNum.textContent = '6'; | |
| narrationText.textContent = 'Client requests prompt list'; | |
| narration.className = 'narration request-state'; | |
| clientBox.classList.add('active'); | |
| }, | |
| animate: (done) => animateMessage(clientCenter, msgY, serverCenter, msgY, ANIMATION.MSG_DURATION, done), | |
| after: () => { | |
| clientBox.classList.remove('active'); | |
| serverBox.classList.add('active'); | |
| } | |
| }, | |
| // Step 7: prompts/list response | |
| { | |
| setup: () => { | |
| configMessage('response', 'β', 'prompts/list', 'ListPromptsResult', 'left'); | |
| setMessagePos(serverCenter, msgY); | |
| stepNum.textContent = '7'; | |
| narrationText.textContent = 'Server returns ListPromptsResult'; | |
| narration.className = 'narration response-state'; | |
| }, | |
| animate: (done) => animateMessage(serverCenter, msgY, clientCenter, msgY, ANIMATION.MSG_DURATION, done), | |
| after: () => { | |
| serverBox.classList.remove('active'); | |
| } | |
| }, | |
| // Step 8: tools/call request | |
| { | |
| setup: () => { | |
| configMessage('request', 'β', 'tools/call', 'CallToolRequest', 'right'); | |
| setMessagePos(clientCenter, msgY); | |
| stepNum.textContent = '8'; | |
| narrationText.textContent = 'Client calls a tool'; | |
| narration.className = 'narration request-state'; | |
| clientBox.classList.add('active'); | |
| }, | |
| animate: (done) => animateMessage(clientCenter, msgY, serverCenter, msgY, ANIMATION.MSG_DURATION, done), | |
| after: () => { | |
| clientBox.classList.remove('active'); | |
| serverBox.classList.add('active'); | |
| setStatus(serverStatus, 'processing', ''); | |
| } | |
| }, | |
| // Step 9: Server sends sampling request BACK to client (server waits) | |
| { | |
| setup: () => { | |
| configMessage('server-request', 'β', 'sampling/createMessage', 'CreateMessageRequest', 'left'); | |
| setMessagePos(serverCenter, msgY); | |
| stepNum.textContent = '9'; | |
| narrationText.textContent = 'Server needs LLM β sends sampling request to client'; | |
| narration.className = 'narration waiting-state'; | |
| serverBox.classList.remove('active'); | |
| serverBox.classList.add('waiting'); | |
| setStatus(serverStatus, 'WAITING', 'waiting'); | |
| }, | |
| animate: (done) => animateMessage(serverCenter, msgY, clientCenter, msgY, ANIMATION.MSG_DURATION, done), | |
| after: () => { | |
| clientBox.classList.add('active'); | |
| } | |
| }, | |
| // Step 10: Client responds with sampling result | |
| { | |
| setup: () => { | |
| configMessage('client-response', 'β', 'sampling/createMessage', 'CreateMessageResult', 'right'); | |
| setMessagePos(clientCenter, msgY); | |
| stepNum.textContent = '10'; | |
| narrationText.textContent = 'Client returns LLM response β server resumes'; | |
| narration.className = 'narration response-state'; | |
| }, | |
| animate: (done) => animateMessage(clientCenter, msgY, serverCenter, msgY, ANIMATION.MSG_DURATION, done), | |
| after: () => { | |
| clientBox.classList.remove('active'); | |
| serverBox.classList.remove('waiting'); | |
| serverBox.classList.add('active'); | |
| setStatus(serverStatus, 'processing', ''); | |
| } | |
| }, | |
| // Step 11: tools/call response | |
| { | |
| setup: () => { | |
| configMessage('response', 'β', 'tools/call', 'CallToolResult', 'left'); | |
| setMessagePos(serverCenter, msgY); | |
| stepNum.textContent = '11'; | |
| narrationText.textContent = 'Server completes tool call'; | |
| narration.className = 'narration response-state'; | |
| }, | |
| animate: (done) => animateMessage(serverCenter, msgY, clientCenter, msgY, ANIMATION.MSG_DURATION, done), | |
| after: () => { | |
| serverBox.classList.remove('active'); | |
| setStatus(serverStatus, '', ''); | |
| } | |
| }, | |
| // Step 12: Pause before restart | |
| { | |
| setup: () => { | |
| message.className = 'message'; | |
| stepNum.textContent = 'β'; | |
| narrationText.textContent = 'Connection complete β both sides know capabilities'; | |
| narration.className = 'narration success-state'; | |
| }, | |
| animate: (done) => setTimeout(done, ANIMATION.ERROR_PAUSE), | |
| after: () => {} | |
| } | |
| ]; | |
| runStepSequence(steps); | |
| scaleCanvas(); | |
| </script> | |
| </body> | |
| </html> | |