| 1 | package plugin |
| 2 | |
| 3 | import ( |
| 4 | "crypto/sha256" |
| 5 | "encoding/hex" |
| 6 | "encoding/json" |
| 7 | "fmt" |
| 8 | ) |
| 9 | |
| 10 | // HostProfile is the semantic identity of a frontend's MCP client surface: |
| 11 | // which optional client capabilities every server connection declares. It is |
| 12 | // fixed when the Host is created and never changes for the Host's lifetime — |
| 13 | // capabilities are negotiated per connection at initialize time, so a live |
| 14 | // downgrade would require tearing every session down. Cache identity and the |
| 15 | // capability matrix both derive from the profile, never from SDK versions, |
| 16 | // wall-clock time, or negotiated results. |
| 17 | type HostProfile string |
| 18 | |
| 19 | const ( |
| 20 | // HostProfileCore is the headless surface: bots and print-mode CLI. It |
| 21 | // declares no optional interaction capabilities, matching the legacy |
| 22 | // client byte-for-byte, so it keeps the v2 cache contract. |
| 23 | HostProfileCore HostProfile = "core-v1" |
| 24 | // HostProfileInteractive adds form and URL elicitation for frontends with |
| 25 | // a human on the other end: the chat TUI and serve. |
| 26 | HostProfileInteractive HostProfile = "interactive-v1" |
| 27 | // HostProfileDesktopApps is the Desktop surface: elicitation plus the |
| 28 | // stable MCP Apps 2026-01-26 ui extension. |
| 29 | HostProfileDesktopApps HostProfile = "desktop-apps-2026-01-26-v1" |
| 30 | ) |
| 31 | |
| 32 | // AppsUIExtensionID is the client extension identifier for MCP Apps |
| 33 | // (ext-apps 2026-01-26). Declaring it tells servers this host can render |
| 34 | // text/html;profile=mcp-app resources inline. |
| 35 | const AppsUIExtensionID = "io.modelcontextprotocol/ui" |
| 36 | |
| 37 | // AppsMimeType is the single MIME type the Desktop host accepts for app |
| 38 | // resources, matching the stable Apps specification. |
| 39 | const AppsMimeType = "text/html;profile=mcp-app" |
| 40 | |
| 41 | // ProfileCapabilities is the set of optional client capabilities a profile |
| 42 | // declares. The JSON encoding is the profile's cache identity: two profiles |
| 43 | // that declare identical capabilities must share one cache identity, and any |
| 44 | // capability change must change the identity so stale tool catalogs written |
| 45 | // under the old negotiation can never be read under the new one. |
| 46 | type ProfileCapabilities struct { |
| 47 | ElicitationForms bool `json:"elicitationForms,omitempty"` |
| 48 | ElicitationURL bool `json:"elicitationURL,omitempty"` |
| 49 | AppsUI bool `json:"appsUI,omitempty"` |
| 50 | } |
| 51 | |
| 52 | // HostProfileForInteractive maps a human-in-the-loop flag onto the profile: |
| 53 | // interactive-v1 when a human can answer prompts, core-v1 otherwise. |
| 54 | func HostProfileForInteractive(interactive bool) HostProfile { |
| 55 | if interactive { |
| 56 | return HostProfileInteractive |
| 57 | } |
| 58 | return HostProfileCore |
| 59 | } |
| 60 | |
| 61 | // String returns the wire form of the profile identifier. |
| 62 | func (p HostProfile) String() string { return string(p.Normalize()) } |
| 63 | |
| 64 | // Capabilities returns what the profile declares to every server. |
| 65 | func (p HostProfile) Capabilities() ProfileCapabilities { |
| 66 | switch p { |
| 67 | case HostProfileInteractive: |
| 68 | return ProfileCapabilities{ElicitationForms: true, ElicitationURL: true} |
| 69 | case HostProfileDesktopApps: |
| 70 | return ProfileCapabilities{ElicitationForms: true, ElicitationURL: true, AppsUI: true} |
| 71 | default: |
| 72 | return ProfileCapabilities{} |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | // Normalize maps an unknown or empty profile onto core-v1 so a config typo can |
| 77 | // never silently widen the declared surface. |
| 78 | func (p HostProfile) Normalize() HostProfile { |
| 79 | switch p { |
| 80 | case HostProfileInteractive, HostProfileDesktopApps: |
| 81 | return p |
| 82 | default: |
| 83 | return HostProfileCore |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | // UsesEnhancedCache reports whether the profile declares anything beyond the |
| 88 | // legacy client surface. Such profiles cannot reuse the v2 cache: a server may |
| 89 | // return a different tools/list once it sees elicitation or ui extensions, so |
| 90 | // they read and write an isolated v3 cache instead. |
| 91 | func (p HostProfile) UsesEnhancedCache() bool { |
| 92 | return p.Normalize() != HostProfileCore |
| 93 | } |
| 94 | |
| 95 | // ProfileCacheHash is a stable, short digest of the declared capabilities. |
| 96 | // It appears in the enhanced cache filename (<slug>.host-<hash>.json) so |
| 97 | // caches written under different profiles never collide. |
| 98 | func (p HostProfile) ProfileCacheHash() string { |
| 99 | caps := p.Capabilities() |
| 100 | b, err := json.Marshal(caps) |
| 101 | if err != nil { |
| 102 | // ProfileCapabilities is three booleans; marshal cannot fail. |
| 103 | panic(fmt.Sprintf("plugin: marshal profile capabilities: %v", err)) |
| 104 | } |
| 105 | sum := sha256.Sum256(b) |
| 106 | return hex.EncodeToString(sum[:6]) |
| 107 | } |
| 108 | |
| 109 | // HostProfileOrder is the display order of the capability matrix layers. |
| 110 | var HostProfileOrder = []HostProfile{HostProfileCore, HostProfileInteractive, HostProfileDesktopApps} |
| 111 | |
| 112 | // ProfileDisplayNames maps profiles to human labels for status surfaces. |
| 113 | func (p HostProfile) DisplayName() string { |
| 114 | switch p { |
| 115 | case HostProfileInteractive: |
| 116 | return "Interactive Host" |
| 117 | case HostProfileDesktopApps: |
| 118 | return "Apps Host" |
| 119 | default: |
| 120 | return "Core Host" |
| 121 | } |
| 122 | } |
| 123 |