RegistryCard.vue
1 <script setup lang="ts"> 2 import { ref, computed } from 'vue' 3 4 import { McpRegistryPackage, McpRegistryType } from '@/renderer/types/registry' 5 6 // State for loading indicators 7 const loadingServers = ref(false) 8 9 // Query history and current query string 10 const queryHistory = ref<Record<string, McpRegistryType>>({}) 11 const queryString = ref('') 12 const lastQueryString = ref('') 13 14 // Default query limit 15 const queryLimit = '5' 16 17 // Computed property to get the last query result 18 const lastQuery = computed(() => { 19 const last = queryHistory.value[lastQueryString.value] 20 return last || { servers: [] } 21 }) 22 23 // Fetch servers based on search query 24 async function getServers(search: string) { 25 const json = await fetchJson(search) 26 if (json) { 27 queryHistory.value[search] = json 28 lastQueryString.value = search 29 } 30 } 31 32 // Fetch next page of results 33 async function getNext() { 34 const search = lastQueryString.value 35 const nextCursor = lastQuery.value.metadata?.nextCursor 36 if (!nextCursor) return 37 38 const json = await fetchJson(search, nextCursor) 39 if (json?.servers?.length) { 40 queryHistory.value[search].servers.push(...json.servers) 41 queryHistory.value[search].metadata = json.metadata 42 } 43 } 44 45 // Helper function to fetch JSON data 46 async function fetchJson(search: string, cursor?: string) { 47 loadingServers.value = true 48 try { 49 const baseUrl = 'https://registry.modelcontextprotocol.io/v0/servers' 50 const url = new URL(baseUrl) 51 if (search) url.searchParams.append('search', search) 52 url.searchParams.append('limit', queryLimit) 53 url.searchParams.append('version', 'latest') 54 if (cursor) url.searchParams.append('cursor', cursor) 55 56 const res = await fetch(url.toString()) 57 if (!res.ok) throw new Error(`HTTP error! Status: ${res.status}`) 58 59 return await res.json() 60 } finally { 61 loadingServers.value = false 62 } 63 } 64 65 // Generate package URL based on registry type 66 function getPackageUrl(registry: McpRegistryPackage) { 67 switch (registry.registryType) { 68 case 'mcpb': 69 return registry.identifier 70 case 'npm': 71 if (!registry.registryBaseUrl || registry.registryBaseUrl.includes('npmjs.org')) { 72 return `https://www.npmjs.com/package/${registry.identifier}` 73 } 74 case 'oci': 75 if (!registry.registryBaseUrl || registry.registryBaseUrl.includes('docker.io')) { 76 return `https://hub.docker.com/r/${registry.identifier}` 77 } 78 case 'pypi': 79 if (!registry.registryBaseUrl || registry.registryBaseUrl.includes('pypi.org')) { 80 return `https://pypi.org/project/${registry.identifier}` 81 } 82 default: 83 return registry.registryBaseUrl ?? undefined 84 } 85 } 86 </script> 87 88 <template> 89 <v-card> 90 <template #title> 91 <v-text-field 92 v-model="queryString" 93 :loading="loadingServers" 94 class="mr-4" 95 prepend-inner-icon="mdi-server" 96 variant="outlined" 97 clearable 98 hide-details 99 @keyup.enter="getServers(queryString)" 100 ></v-text-field> 101 </template> 102 <template #append> 103 <v-btn 104 :loading="loadingServers" 105 rounded="lg" 106 variant="tonal" 107 icon="mdi-magnify" 108 @click="getServers(queryString)" 109 ></v-btn> 110 </template> 111 112 <v-divider></v-divider> 113 <v-card-text> 114 <v-card class="mt-4"> 115 <v-data-iterator :key="lastQueryString" :items="lastQuery.servers" :items-per-page="-1"> 116 <template #default="{ items }"> 117 <v-expansion-panels variant="accordion" :rounded="false"> 118 <v-expansion-panel v-for="{ raw: { server } } in items" :key="server.name"> 119 <v-expansion-panel-title> 120 <v-list-item> 121 <v-list-item-title class="d-flex" 122 >{{ server.name }} 123 124 <v-chip size="small" class="ml-3 mb-2 font-weight-bold" color="primary"> 125 {{ server.version }} 126 </v-chip> 127 128 <v-icon 129 v-if="server.packages" 130 class="ml-2" 131 icon="mdi-desktop-classic" 132 color="brown-lighten-2" 133 ></v-icon> 134 135 <v-icon 136 v-if="server.remotes" 137 class="ml-2" 138 icon="mdi-web" 139 color="blue-lighten-1" 140 ></v-icon> 141 </v-list-item-title> 142 <v-list-item-subtitle class="text-high-emphasis">{{ 143 server.description 144 }}</v-list-item-subtitle> 145 </v-list-item> 146 </v-expansion-panel-title> 147 <v-expansion-panel-text> 148 <v-col v-if="server.repository?.url"> 149 <v-card 150 color="green-lighten-1" 151 class="mx-auto" 152 :subtitle="server.repository.url" 153 :title="server.repository.source" 154 prepend-icon="mdi-home" 155 append-icon="mdi-open-in-new" 156 :href="server.repository.url" 157 target="_blank" 158 ></v-card> 159 </v-col> 160 <v-col v-if="server.packages"> 161 <v-card 162 v-for="regPackage in server.packages" 163 :key="regPackage.registryType" 164 color="brown-lighten-2" 165 class="mx-auto my-1" 166 prepend-icon="mdi-desktop-classic" 167 :subtitle="regPackage.identifier" 168 :title=" 169 regPackage.registryType + 170 (regPackage.transport ? ` - ${regPackage.transport.type}` : '') 171 " 172 append-icon="mdi-open-in-new" 173 :href="getPackageUrl(regPackage)" 174 target="_blank" 175 ></v-card> 176 </v-col> 177 <v-col v-if="server.remotes"> 178 <v-card 179 v-for="remote in server.remotes" 180 :key="remote.url" 181 color="blue-lighten-1" 182 class="mx-auto my-1" 183 prepend-icon="mdi-web" 184 :subtitle="remote.url" 185 :title="remote.type" 186 ></v-card> 187 </v-col> 188 <!-- <v-textarea :model-value="JSON.stringify(item.raw, null, 2)" variant="plain" auto-grow 189 readonly></v-textarea> --> 190 </v-expansion-panel-text> 191 </v-expansion-panel> 192 </v-expansion-panels> 193 </template> 194 </v-data-iterator> 195 </v-card> 196 </v-card-text> 197 198 <v-divider></v-divider> 199 <v-card-actions> 200 <v-footer 201 class="ml-4 mr-2 justify-space-between text-body-2" 202 color="surface-variant" 203 rounded="sm" 204 > 205 {{ $t('mcp.total') }}: {{ lastQuery.servers.length }} 206 </v-footer> 207 208 <v-btn 209 :disabled="!lastQuery.metadata?.nextCursor" 210 :loading="loadingServers" 211 rounded="lg" 212 icon="mdi-book-open-page-variant" 213 color="secondary" 214 variant="plain" 215 @click="getNext" 216 ></v-btn> 217 <slot></slot> 218 </v-card-actions> 219 </v-card> 220 </template> 221 222 <style scoped></style>