Skip to content

Commit 2d1c6d2

Browse files
committed
Supports discovery and preview of MCP servers from the registry
1 parent ff4b6c7 commit 2d1c6d2

File tree

6 files changed

+290
-5
lines changed

6 files changed

+290
-5
lines changed

src/renderer/components/common/ConfigDxtCard.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ const getErrorMessages = (para: McpbUserConfigurationOption, key: string) => {
9999
<template #title>
100100
<div class="d-flex">
101101
{{ $t('dxt.title') + ' - ' + metadata.name }}
102-
<v-chip size="small" class="ml-2 mt-1 font-weight-bold" color="blue">
102+
<v-chip size="small" class="ml-2 mt-1 font-weight-bold" color="primary">
103103
{{ manifest.version }}
104104
</v-chip>
105105
</div>
@@ -237,7 +237,7 @@ const getErrorMessages = (para: McpbUserConfigurationOption, key: string) => {
237237
<v-row class="mx-1 mt-2 mb-2" dense>
238238
<v-col v-if="manifest.author" cols="12" md="6">
239239
<v-card
240-
color="blue-lighten-1"
240+
color="indigo-lighten-2"
241241
append-icon="mdi-open-in-new"
242242
class="mx-auto"
243243
:href="manifest.author.url"
@@ -249,7 +249,7 @@ const getErrorMessages = (para: McpbUserConfigurationOption, key: string) => {
249249
</v-col>
250250
<v-col v-if="manifest.repository" cols="12" md="6">
251251
<v-card
252-
color="indigo-lighten-2"
252+
color="light-green-darken-1"
253253
append-icon="mdi-open-in-new"
254254
class="mx-auto"
255255
:href="manifest.repository.url"
@@ -261,7 +261,7 @@ const getErrorMessages = (para: McpbUserConfigurationOption, key: string) => {
261261
</v-col>
262262
<v-col v-if="manifest.homepage" cols="12" md="6">
263263
<v-card
264-
color="green-lighten-1"
264+
color="blue-lighten-1"
265265
append-icon="mdi-open-in-new"
266266
class="mx-auto"
267267
:href="manifest.homepage"
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
<script setup lang="ts">
2+
import { ref, computed } from 'vue'
3+
4+
type McpRegistryPackage = {
5+
identifier: string
6+
registryType: string
7+
registryBaseUrl?: string
8+
}
9+
10+
const loadingServers = ref(false)
11+
12+
const queryHistory = ref({})
13+
const queryString = ref('')
14+
const lastQueryString = ref('')
15+
16+
const queryLimit = '5'
17+
18+
const lastQuery = computed(() => {
19+
const last = queryHistory.value[lastQueryString.value]
20+
if (last) {
21+
return last
22+
} else {
23+
return {
24+
servers: []
25+
}
26+
}
27+
})
28+
29+
async function getServers(search: string) {
30+
const json = await fetchJson(search)
31+
32+
if (json) {
33+
queryHistory.value[search] = json
34+
lastQueryString.value = search
35+
}
36+
}
37+
38+
async function getNext() {
39+
const search = lastQueryString.value
40+
41+
const nextCursor = lastQuery.value.metadata?.next_cursor
42+
43+
if (!nextCursor) return
44+
45+
const json = await fetchJson(search, nextCursor)
46+
47+
if (json && json.servers.length > 0) {
48+
queryHistory.value[search].servers.push(...json.servers)
49+
queryHistory.value[search].metadata = json.metadata
50+
}
51+
}
52+
53+
async function fetchJson(search: string, cursor?: string) {
54+
loadingServers.value = true
55+
try {
56+
const baseUrl = 'https://registry.modelcontextprotocol.io/v0/servers'
57+
const url = new URL(baseUrl)
58+
if (search) url.searchParams.append('search', search)
59+
url.searchParams.append('limit', queryLimit)
60+
61+
if (cursor) url.searchParams.append('cursor', cursor)
62+
63+
const res = await fetch(url.toString())
64+
if (!res.ok) throw new Error(`HTTP error! Status: ${res.status}`)
65+
66+
return await res.json()
67+
} finally {
68+
loadingServers.value = false
69+
}
70+
}
71+
72+
function getPackageUrl(registry: McpRegistryPackage) {
73+
switch (registry.registryType) {
74+
case 'mcpb':
75+
return registry.identifier
76+
case 'npm':
77+
if (!registry.registryBaseUrl || registry.registryBaseUrl.includes('npmjs.org')) {
78+
return `https://www.npmjs.com/package/${registry.identifier}`
79+
}
80+
case 'oci':
81+
if (!registry.registryBaseUrl || registry.registryBaseUrl.includes('docker.io')) {
82+
return `https://hub.docker.com/r/${registry.identifier}`
83+
}
84+
case 'pypi':
85+
if (!registry.registryBaseUrl || registry.registryBaseUrl.includes('pypi.org')) {
86+
return `https://pypi.org/project/${registry.identifier}`
87+
}
88+
default:
89+
return registry.registryBaseUrl ?? undefined
90+
}
91+
}
92+
</script>
93+
94+
<template>
95+
<v-card>
96+
<template #title>
97+
<v-text-field
98+
v-model="queryString"
99+
class="mr-4"
100+
prepend-inner-icon="mdi-server"
101+
variant="outlined"
102+
clearable
103+
hide-details
104+
@keyup.enter="getServers(queryString)"
105+
></v-text-field>
106+
</template>
107+
<template #append>
108+
<v-btn
109+
rounded="lg"
110+
variant="tonal"
111+
icon="mdi-magnify"
112+
@click="getServers(queryString)"
113+
></v-btn>
114+
</template>
115+
116+
<v-divider></v-divider>
117+
<v-card-text>
118+
<v-card class="mt-4">
119+
<v-data-iterator :key="lastQueryString" :items="lastQuery.servers" :items-per-page="-1">
120+
<template #default="{ items }">
121+
<v-expansion-panels variant="accordion" :rounded="false">
122+
<v-expansion-panel v-for="item in items as any" :key="item.raw.name">
123+
<v-expansion-panel-title>
124+
<v-list-item>
125+
<v-list-item-title class="d-flex"
126+
>{{ item.raw.name }}
127+
128+
<v-chip size="small" class="ml-3 mb-2 font-weight-bold" color="primary">
129+
{{ item.raw.version }}
130+
</v-chip>
131+
132+
<v-icon
133+
v-if="item.raw.packages"
134+
class="ml-2"
135+
icon="mdi-desktop-classic"
136+
color="brown-lighten-2"
137+
></v-icon>
138+
139+
<v-icon
140+
v-if="item.raw.remotes"
141+
class="ml-2"
142+
icon="mdi-web"
143+
color="blue-lighten-1"
144+
></v-icon>
145+
</v-list-item-title>
146+
<v-list-item-subtitle class="text-high-emphasis">{{
147+
item.raw.description
148+
}}</v-list-item-subtitle>
149+
</v-list-item>
150+
</v-expansion-panel-title>
151+
<v-expansion-panel-text>
152+
<v-col v-if="item.raw.repository">
153+
<v-card
154+
color="light-green-darken-1"
155+
class="mx-auto"
156+
:subtitle="item.raw.repository.url"
157+
:title="item.raw.repository.source"
158+
prepend-icon="mdi-home"
159+
append-icon="mdi-open-in-new"
160+
:href="item.raw.repository.url"
161+
target="_blank"
162+
></v-card>
163+
</v-col>
164+
<v-col v-if="item.raw.packages">
165+
<v-card
166+
v-for="regPackage in item.raw.packages"
167+
:key="regPackage.registryType"
168+
color="brown-lighten-2"
169+
class="mx-auto my-1"
170+
prepend-icon="mdi-desktop-classic"
171+
:subtitle="regPackage.identifier"
172+
:title="
173+
regPackage.registryType +
174+
(regPackage.transport ? ` - ${regPackage.transport.type}` : '')
175+
"
176+
append-icon="mdi-open-in-new"
177+
:href="getPackageUrl(regPackage)"
178+
target="_blank"
179+
></v-card>
180+
</v-col>
181+
<v-col v-if="item.raw.remotes">
182+
<v-card
183+
v-for="remote in item.raw.remotes"
184+
:key="remote.url"
185+
color="blue-lighten-1"
186+
class="mx-auto my-1"
187+
prepend-icon="mdi-web"
188+
:subtitle="remote.url"
189+
:title="remote.type"
190+
></v-card>
191+
</v-col>
192+
<!-- <v-textarea :model-value="JSON.stringify(item.raw, null, 2)" variant="plain" auto-grow
193+
readonly></v-textarea> -->
194+
</v-expansion-panel-text>
195+
</v-expansion-panel>
196+
</v-expansion-panels>
197+
</template>
198+
</v-data-iterator>
199+
</v-card>
200+
</v-card-text>
201+
202+
<v-divider></v-divider>
203+
<v-card-actions>
204+
<v-footer
205+
class="ml-4 mr-2 justify-space-between text-body-2"
206+
color="surface-variant"
207+
rounded="sm"
208+
>
209+
{{ $t('mcp.total') }}: {{ lastQuery.servers.length }}
210+
</v-footer>
211+
212+
<v-btn
213+
:disabled="!lastQuery.metadata?.next_cursor"
214+
rounded="lg"
215+
icon="mdi-book-open-page-variant"
216+
color="secondary"
217+
variant="plain"
218+
@click="getNext"
219+
></v-btn>
220+
<slot></slot>
221+
</v-card-actions>
222+
</v-card>
223+
</template>
224+
225+
<style scoped></style>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<script setup lang="ts">
2+
import { ref, watch } from 'vue'
3+
4+
import RegistryCard from '@/renderer/components/common/RegistryCard.vue'
5+
6+
const props = defineProps({
7+
modelValue: {
8+
type: Boolean,
9+
required: true
10+
}
11+
})
12+
13+
const emit = defineEmits(['update:modelValue'])
14+
15+
const internalDialog = ref(props.modelValue)
16+
17+
watch(
18+
() => props.modelValue,
19+
(newVal) => {
20+
internalDialog.value = newVal
21+
}
22+
)
23+
24+
watch(internalDialog, (newVal) => {
25+
emit('update:modelValue', newVal)
26+
})
27+
28+
const closeDialog = () => {
29+
internalDialog.value = false
30+
}
31+
</script>
32+
33+
<template>
34+
<v-dialog v-model="internalDialog" persistent max-width="80vw" max-height="80vh" scrollable>
35+
<RegistryCard>
36+
<v-btn
37+
variant="plain"
38+
rounded="lg"
39+
icon="mdi-close-box"
40+
color="error"
41+
@click="closeDialog"
42+
></v-btn>
43+
</RegistryCard>
44+
</v-dialog>
45+
</template>
46+
<style scoped></style>

src/renderer/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
"new": "New MCP Server",
4747
"init": "Start MCP Servers",
4848
"stop": "Stop MCP Servers",
49+
"search": "MCP Registry Lookup",
50+
"total": "Total Servers",
4951
"read": "Read",
5052
"minutes": "minutes",
5153
"open": "Reveal in File Explorer",

src/renderer/locales/zh.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
"new": "新的 MCP 服务端",
4747
"init": "启动 MCP 服务端",
4848
"stop": "停止 MCP 服务端",
49+
"search": "MCP 注册中心查询",
50+
"total": "服务端总数",
4951
"read": "阅读",
5052
"minutes": "分钟",
5153
"open": "在文件管理器中打开",
@@ -90,7 +92,7 @@
9092
},
9193
"resource": {
9294
"list": "资源列表",
93-
"total": "全部资源"
95+
"total": "资源总数"
9496
},
9597
"sampling": {
9698
"title": "采样",

src/renderer/screens/mcp/McpSideDock.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useLayoutStore } from '@/renderer/store/layout'
66
import { useSnackbarStore } from '@/renderer/store/snackbar'
77
import McpDxtPage from '@/renderer/components/pages/McpDxtPage.vue'
88
import McpAddPage from '@/renderer/components/pages/McpAddPage.vue'
9+
import McpRegistryPage from '@/renderer/components/pages/McpRegistryPage.vue'
910
import { pick } from 'lodash'
1011
1112
const snackbarStore = useSnackbarStore()
@@ -18,6 +19,8 @@ const dxtDialog = ref(false)
1819
1920
const addDialog = ref(false)
2021
22+
const registryDialog = ref(false)
23+
2124
const isDragActive = ref(false)
2225
2326
async function stopAllMcpServers() {
@@ -66,6 +69,10 @@ const addConfig = () => {
6669
addDialog.value = true
6770
}
6871
72+
const searchRegistry = () => {
73+
registryDialog.value = true
74+
}
75+
6976
document.addEventListener('dragenter', (_e) => {
7077
if (!dxtDialog.value) {
7178
isDragActive.value = true
@@ -128,10 +135,13 @@ const items = [
128135
</v-list-item>
129136
</v-list>
130137
</v-menu>
138+
<v-btn v-tooltip:top="$t('mcp.search')" icon="mdi-home-search" @click="searchRegistry">
139+
</v-btn>
131140
</v-btn-group>
132141
<McpDxtPage v-model="dxtDialog"></McpDxtPage>
133142

134143
<McpAddPage v-model="addDialog"></McpAddPage>
144+
<McpRegistryPage v-model="registryDialog"></McpRegistryPage>
135145
</v-container>
136146
</template>
137147

0 commit comments

Comments
 (0)