Skip to content

Commit ff3cdd4

Browse files
committed
Enables traversal of a resource list via cursor
1 parent abc05a5 commit ff3cdd4

File tree

6 files changed

+210
-61
lines changed

6 files changed

+210
-61
lines changed
Lines changed: 111 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,117 @@
11
<script setup lang="ts">
2+
import { computed } from 'vue'
23
import { useMcpStore } from '@/renderer/store/mcp'
3-
import { useResourceStore } from '@/renderer/store/resource'
4+
import { useResourceStore, emptyItem } from '@/renderer/store/resource'
45
const mcpStore = useMcpStore()
56
const resourceStore = useResourceStore()
7+
8+
const currentServerName = computed(() => {
9+
return mcpStore.selected?.[0]
10+
})
11+
12+
const currentResource = computed(() => {
13+
return resourceStore.data[currentServerName.value]?.resource ?? emptyItem
14+
})
15+
16+
const currentTemplate = computed(() => {
17+
return resourceStore.data[currentServerName.value]?.template ?? emptyItem
18+
})
619
</script>
720

821
<template>
922
<v-data-iterator
10-
:items="resourceStore.resourceTemplatesList"
11-
items-per-page="-1"
23+
:items="currentTemplate.list"
24+
:items-per-page="-1"
1225
:loading="resourceStore.loadingTemplates"
13-
@update:options="resourceStore.loadTemplates()"
26+
@update:options="resourceStore.loadTemplates(currentServerName)"
1427
>
1528
<template #default="{ items }">
16-
<v-container>
17-
<v-row dense>
18-
<v-col
19-
v-for="item in items as any"
20-
:key="item.raw.uriTemplate + ':' + item.raw.name"
21-
cols="auto"
22-
class="flex-fill"
23-
>
24-
<v-card border flat>
25-
<v-card-item :subtitle="item.raw.uriTemplate" class="mb-2" :title="item.raw.name">
26-
</v-card-item>
27-
<v-card-text>{{ item.raw.description }}</v-card-text>
28-
</v-card>
29-
</v-col>
30-
</v-row>
31-
</v-container>
29+
<v-row dense>
30+
<v-col
31+
v-for="item in items as any"
32+
:key="item.raw.uriTemplate + ':' + item.raw.name"
33+
cols="auto"
34+
class="flex-fill"
35+
>
36+
<v-card border flat>
37+
<v-card-item :subtitle="item.raw.uriTemplate" class="mb-2" :title="item.raw.name">
38+
</v-card-item>
39+
<v-card-text>{{ item.raw.description }}</v-card-text>
40+
</v-card>
41+
</v-col>
42+
</v-row>
3243
</template>
3344
</v-data-iterator>
34-
<v-data-iterator
35-
:key="mcpStore.getSelected"
36-
:items="resourceStore.resourceList"
37-
items-per-page="-1"
38-
:loading="resourceStore.loadingResources"
39-
@update:options="resourceStore.loadResources()"
40-
>
41-
<template #default="{ items }">
42-
<v-container>
43-
<v-expansion-panels>
45+
<v-card class="mt-4">
46+
<v-data-iterator
47+
:key="mcpStore.getSelected"
48+
:items="currentResource.list"
49+
:items-per-page="currentResource.perPage"
50+
:loading="resourceStore.loadingResources"
51+
@update:options="resourceStore.loadResources(currentServerName)"
52+
>
53+
<template #header="{ page, pageCount, prevPage, nextPage }">
54+
<v-toolbar color="white">
55+
<v-toolbar-title :text="$t('resource.list')" class="text-h6"></v-toolbar-title>
56+
57+
<v-btn
58+
:disabled="page === 1"
59+
class="me-2"
60+
rounded="lg"
61+
icon="mdi-arrow-left"
62+
size="small"
63+
variant="tonal"
64+
@click="prevPage"
65+
></v-btn>
66+
<v-btn
67+
class="me-3"
68+
rounded="lg"
69+
icon="mdi-arrow-right"
70+
size="small"
71+
variant="tonal"
72+
:disabled="page === pageCount && !currentResource.cursor"
73+
@click="
74+
page === pageCount
75+
? resourceStore.getNextpage(currentServerName).then(() => {
76+
nextPage()
77+
})
78+
: nextPage()
79+
"
80+
></v-btn>
81+
</v-toolbar>
82+
83+
<!-- <span class="d-flex justify-space-between mb-2 align-center">
84+
<div class="text-truncate">
85+
Most popular mice
86+
</div>
87+
88+
<div class="d-flex align-center">
89+
<div class="d-inline-flex">
90+
<v-btn
91+
:disabled="page === 1"
92+
class="me-2"
93+
rounded="lg"
94+
icon="mdi-arrow-left"
95+
size="small"
96+
variant="tonal"
97+
@click="prevPage"
98+
></v-btn>
99+
<v-btn
100+
rounded="lg"
101+
icon="mdi-arrow-right"
102+
size="small"
103+
variant="tonal"
104+
:disabled="page === pageCount && !resourceStore.resourceCursor"
105+
@click="page === pageCount ? resourceStore.getNextpage().then(() => {nextPage()}) : nextPage()"
106+
></v-btn>
107+
</div>
108+
</div>
109+
</span> -->
110+
<v-divider></v-divider>
111+
</template>
112+
113+
<template #default="{ items }">
114+
<v-expansion-panels variant="accordion" :rounded="false">
44115
<v-expansion-panel
45116
v-for="item in items as any"
46117
:key="item.raw.uri + ':' + item.raw.name"
@@ -56,7 +127,14 @@ const resourceStore = useResourceStore()
56127
</v-expansion-panel-text>
57128
</v-expansion-panel>
58129
</v-expansion-panels>
59-
</v-container>
60-
</template>
61-
</v-data-iterator>
130+
</template>
131+
<template #footer="{ page, pageCount }">
132+
<v-footer class="justify-space-between text-body-2" color="surface-variant">
133+
{{ $t('resource.total') }}: {{ currentResource.list.length }}
134+
135+
<div> {{ page }} / {{ pageCount }} </div>
136+
</v-footer>
137+
</template>
138+
</v-data-iterator>
139+
</v-card>
62140
</template>

src/renderer/locales/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@
7474
"title": "Server Prompt",
7575
"get": "Get"
7676
},
77+
"resource": {
78+
"list": "Resource List",
79+
"total": "Total Resources"
80+
},
7781
"agent": {
7882
"config": "Agent Config",
7983
"prompt": "System Prompt",

src/renderer/locales/zh.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@
8686
"title": "服务端提示模板",
8787
"get": "获取"
8888
},
89+
"resource": {
90+
"list": "资源列表",
91+
"total": "全部资源"
92+
},
8993
"sampling": {
9094
"title": "采样",
9195
"comp": "开始采样",

src/renderer/screens/PopupScreen.vue

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,7 @@ const commandNotify = (item: PopupPromptsType) => {
4242
<v-main class="d-flex justify-center" scrollable>
4343
<v-container class="px-0 pb-0">
4444
<v-sheet>
45-
<v-data-iterator
46-
:items="promptData"
47-
:items-per-page="-1"
48-
:search="search"
49-
>
45+
<v-data-iterator :items="promptData" :items-per-page="-1" :search="search">
5046
<template #default="{ items }">
5147
<v-list density="compact" nav>
5248
<v-list-item

src/renderer/store/mcp.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,21 +132,13 @@ export const useMcpStore = defineStore('mcpStore', {
132132
return servers
133133
},
134134
getServerFunction: function (options: {
135-
serverName?: string
135+
serverName: string
136136
primitiveName: string
137137
methodName: string
138138
}): Function | null {
139139
const { serverName, primitiveName, methodName } = options
140140

141-
let targetServerName
142-
143-
if (serverName) {
144-
targetServerName = serverName
145-
} else {
146-
targetServerName = this.selected?.[0]
147-
}
148-
149-
const allPrimitives = this.getAllByServer(targetServerName)
141+
const allPrimitives = this.getAllByServer(serverName)
150142

151143
const foundItem = allPrimitives.find((item) => item.primitive === primitiveName)
152144

src/renderer/store/resource.ts

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,123 @@
11
import { defineStore } from 'pinia'
22
import { useMcpStore } from '@/renderer/store/mcp'
33

4-
export const useResourceStore = defineStore('resourceStore', {
5-
state: () => ({
6-
resourceDialog: false,
7-
tab: null,
4+
import { ListResourcesResult, ListResourceTemplatesResult } from '@modelcontextprotocol/sdk/types'
5+
6+
type ResourceTemplatesType = ListResourceTemplatesResult['resourceTemplates']
7+
8+
type ResourcesType = ListResourcesResult['resources']
9+
10+
type CursorType = ListResourcesResult['nextCursor']
11+
12+
type ResourceRecordType = {
13+
resource: ResourceItemType
14+
template: ResourceTemplateType
15+
}
16+
17+
type ResourceItemType = {
18+
perPage: number
19+
cursor: CursorType
20+
list: ResourcesType
21+
}
822

9-
resourceList: [],
10-
resourceTemplatesList: [],
23+
type ResourceTemplateType = {
24+
perPage: number
25+
cursor: CursorType
26+
list: ResourceTemplatesType
27+
}
1128

29+
export const emptyItem: ResourceItemType | ResourceTemplateType = {
30+
perPage: -1,
31+
cursor: undefined,
32+
list: []
33+
}
34+
35+
export const useResourceStore = defineStore('resourceStore', {
36+
state: () => ({
37+
data: {} as Record<string, ResourceRecordType>,
1238
loadingTemplates: false,
1339
loadingResources: false
1440
}),
1541
actions: {
16-
loadTemplates: function () {
42+
loadTemplates: function (serverName: string) {
1743
this.loadingTemplates = true
1844
const mcpStore = useMcpStore()
1945
const resourceFunction = mcpStore.getServerFunction({
46+
serverName,
2047
primitiveName: 'resources',
2148
methodName: 'templates/list'
2249
})
2350
try {
24-
resourceFunction().then((result) => {
51+
resourceFunction().then((result: ListResourceTemplatesResult) => {
2552
console.log(result)
26-
this.resourceTemplatesList = result.resourceTemplates
53+
this.data[serverName] = this.data[serverName] || {}
54+
this.data[serverName].template = {
55+
perPage: -1,
56+
cursor: result.nextCursor,
57+
list: result.resourceTemplates
58+
}
2759
})
2860
} catch (error) {
2961
console.error('Failed to load resource templates:', error)
3062
} finally {
3163
this.loadingTemplates = false
3264
}
3365
},
34-
loadResources: function () {
66+
loadResources: async function (serverName: string) {
67+
// Already loaded
68+
if (this.data[serverName] && 'resource' in this.data[serverName]) return
69+
3570
this.loadingResources = true
3671
const mcpStore = useMcpStore()
3772
const resourceFunction = mcpStore.getServerFunction({
73+
serverName,
3874
primitiveName: 'resources',
3975
methodName: 'list'
4076
})
77+
4178
try {
42-
resourceFunction().then((result) => {
43-
console.log(result)
44-
this.resourceList = result.resources
79+
const result: ListResourcesResult = await resourceFunction()
80+
console.log(result)
81+
const resources: ResourcesType = result.resources
82+
this.data[serverName] = this.data[serverName] || {}
83+
this.data[serverName].resource = {
84+
perPage: resources.length,
85+
cursor: result.nextCursor,
86+
list: resources
87+
}
88+
} catch (error) {
89+
console.error('Failed to load resources:', error)
90+
} finally {
91+
this.loadingResources = false
92+
}
93+
},
94+
getNextpage: async function (serverName: string) {
95+
if (!this.data[serverName]?.resource?.cursor) {
96+
return
97+
}
98+
this.loadingResources = true
99+
const currentResource = this.data[serverName].resource
100+
101+
try {
102+
const mcpStore = useMcpStore()
103+
const resourceFunction = mcpStore.getServerFunction({
104+
serverName,
105+
primitiveName: 'resources',
106+
methodName: 'list'
107+
})
108+
109+
const result: ListResourcesResult = await resourceFunction({
110+
method: 'resources/list',
111+
params: {
112+
cursor: currentResource.cursor
113+
}
45114
})
115+
116+
console.log(result)
117+
const resources: ResourcesType = result.resources
118+
119+
currentResource.list = [...currentResource.list, ...resources]
120+
currentResource.cursor = result.nextCursor
46121
} catch (error) {
47122
console.error('Failed to load resources:', error)
48123
} finally {

0 commit comments

Comments
 (0)