/ src / renderer / components / common / RegistryCard.vue
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>