feat: create mcp

This commit is contained in:
Quicy
2026-01-22 11:21:48 +08:00
parent 9980fed90b
commit 3afba6974e
11 changed files with 482 additions and 5 deletions
@@ -0,0 +1,26 @@
<script setup lang="ts">
import type { TagsInputRootEmits, TagsInputRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { TagsInputRoot, useForwardPropsEmits } from "reka-ui"
import { cn } from '#/lib/utils'
const props = defineProps<TagsInputRootProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<TagsInputRootEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<TagsInputRoot
v-slot="slotProps" v-bind="forwarded" :class="cn(
'flex flex-wrap gap-2 items-center rounded-md border border-input bg-background px-2 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none',
'focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
props.class)"
>
<slot v-bind="slotProps" />
</TagsInputRoot>
</template>
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { TagsInputInputProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { TagsInputInput, useForwardProps } from "reka-ui"
import { cn } from '#/lib/utils'
const props = defineProps<TagsInputInputProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<TagsInputInput v-bind="forwardedProps" :class="cn('text-sm min-h-5 focus:outline-none flex-1 bg-transparent px-1', props.class)" />
</template>
@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { TagsInputItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { TagsInputItem, useForwardProps } from "reka-ui"
import { cn } from '#/lib/utils'
const props = defineProps<TagsInputItemProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<TagsInputItem v-bind="forwardedProps" :class="cn('flex h-5 items-center rounded-md bg-secondary data-[state=active]:ring-ring data-[state=active]:ring-2 data-[state=active]:ring-offset-2 ring-offset-background', props.class)">
<slot />
</TagsInputItem>
</template>
@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { TagsInputItemDeleteProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import { TagsInputItemDelete, useForwardProps } from "reka-ui"
import { cn } from '#/lib/utils'
const props = defineProps<TagsInputItemDeleteProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<TagsInputItemDelete v-bind="forwardedProps" :class="cn('flex rounded bg-transparent mr-1', props.class)">
<slot>
<X class="w-4 h-4" />
</slot>
</TagsInputItemDelete>
</template>
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { TagsInputItemTextProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { TagsInputItemText, useForwardProps } from "reka-ui"
import { cn } from '#/lib/utils'
const props = defineProps<TagsInputItemTextProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<TagsInputItemText v-bind="forwardedProps" :class="cn('py-0.5 px-2 text-sm rounded bg-transparent', props.class)" />
</template>
@@ -0,0 +1,5 @@
export { default as TagsInput } from "./TagsInput.vue"
export { default as TagsInputInput } from "./TagsInputInput.vue"
export { default as TagsInputItem } from "./TagsInputItem.vue"
export { default as TagsInputItemDelete } from "./TagsInputItemDelete.vue"
export { default as TagsInputItemText } from "./TagsInputItemText.vue"
+1
View File
@@ -31,5 +31,6 @@ export * from './components/spinner/index'
export * from './components/switch/index'
export * from './components/table/index'
export * from './components/tabs/index'
export * from './components/tags-input/index'
export * from './components/textarea/index'
export * from './components/tooltip/index'
@@ -0,0 +1,308 @@
<template>
<section class="flex">
<Dialog v-model:open="open">
<DialogTrigger as-child>
<Button
variant="default"
class="ml-auto my-4"
>
添加MCP
</Button>
</DialogTrigger>
<DialogContent class="sm:max-w-106.25">
<form @submit="createMCP">
<DialogHeader>
<DialogTitle>添加MCP</DialogTitle>
<DialogDescription class="mb-4">
添加MCP完成操作
</DialogDescription>
</DialogHeader>
<div>
<FormField
v-slot="{ componentField }"
name="name"
>
<FormItem>
<FormLabel class="mb-2">
Name
</FormLabel>
<FormControl>
<Input
type="text"
placeholder="请输入Name"
v-bind="componentField"
autocomplete="name"
/>
</FormControl>
<blockquote class="h-5">
<FormMessage />
</blockquote>
</FormItem>
</FormField>
<FormField
v-slot="{ componentField }"
name="config.type"
>
<FormItem>
<FormLabel class="mb-2">
Type
</FormLabel>
<FormControl>
<Select v-bind="componentField">
<SelectTrigger class="w-full">
<SelectValue placeholder="请选择 Type" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="stdio">
Stdio
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<blockquote class="h-5">
<FormMessage />
</blockquote>
</FormItem>
</FormField>
<FormField
v-slot="{ componentField }"
name="config.cwd"
>
<FormItem>
<FormLabel class="mb-2">
Cwd
</FormLabel>
<FormControl>
<Input
type="text"
placeholder="请输入cwd"
v-bind="componentField"
autocomplete="cwd"
/>
</FormControl>
<blockquote class="h-5">
<FormMessage />
</blockquote>
</FormItem>
</FormField>
<FormField
v-slot="{ componentField }"
name="config.command"
>
<FormItem>
<FormLabel class="mb-2">
Command
</FormLabel>
<FormControl>
<Input
placeholder="请输入Command"
v-bind="componentField"
/>
</FormControl>
<blockquote class="h-5">
<FormMessage />
</blockquote>
</FormItem>
</FormField>
<FormField
v-slot="{ componentField }"
name="config.args"
>
<FormItem>
<FormLabel class="mb-2">
Arguments
</FormLabel>
<FormControl>
<TagsInput
v-model="componentField.modelValue"
:add-on-blur="true"
:duplicate="true"
@update:model-value="componentField['onUpdate:modelValue']"
>
<TagsInputItem
v-for="item in componentField.modelValue"
:key="item"
:value="item"
>
<TagsInputItemText />
<TagsInputItemDelete />
</TagsInputItem>
<TagsInputInput
placeholder="请输入Arguments"
class="w-full py-1"
/>
</TagsInput>
</FormControl>
<blockquote class="h-5">
<FormMessage />
</blockquote>
</FormItem>
</FormField>
<FormField
v-slot="{ componentField }"
name="config.env"
>
<FormItem>
<FormLabel class="mb-2">
Env
</FormLabel>
<FormControl>
<TagsInput
:add-on-blur="true"
:model-value="envList"
:convert-value="tagStr => {
if (/^\w+\:\w+$/.test(tagStr)) {
return tagStr
}
return ''
}"
@update:model-value="(env) => {
envList = env.filter(Boolean) as string[]
const curEnvObject: { [key in string]: string } = {}
envList.forEach(envItem => {
const [key, value] = envItem.split(`:`);
if (key && value) {
curEnvObject[key] = value
}
})
componentField['onUpdate:modelValue']?.(curEnvObject)
}"
>
<TagsInputItem
v-for="(value, index) in envList"
:key="index"
:value="value as string"
>
<TagsInputItemText />
<TagsInputItemDelete />
</TagsInputItem>
<TagsInputInput
placeholder="请输入Env"
class="w-full py-1"
/>
</TagsInput>
</FormControl>
<blockquote class="h-5">
<FormMessage />
</blockquote>
</FormItem>
</FormField>
<FormField
v-slot="{ componentField }"
name="active"
>
<FormItem>
<FormControl>
<section class="flex gap-4">
<Label
for="airplane-mode"
>开启</Label>
<Switch
id="airplane-mode"
:model-value="componentField.modelValue"
@update:model-value="componentField['onUpdate:modelValue']"
/>
</section>
</FormControl>
<blockquote class="h-5">
<FormMessage />
</blockquote>
</FormItem>
</FormField>
</div>
<DialogFooter class="mt-4">
<DialogClose as-child>
<Button variant="outline">
Cancel
</Button>
</DialogClose>
<Button type="submit">
添加Model
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</section>
</template>
<script setup lang="ts">
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
Input,
FormField,
FormControl,
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
FormItem,
FormLabel,
FormMessage,
TagsInput,
TagsInputInput,
TagsInputItem,
TagsInputItemDelete,
TagsInputItemText,
Switch,
Label
} from '@memoh/ui'
import z from 'zod'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import { ref,inject } from 'vue'
import { useMutation,useQueryCache } from '@pinia/colada'
import request from '@/utils/request'
const validateSchema = toTypedSchema(z.object({
name: z.string().min(1),
config: z.object({
type: z.string().min(1),
command: z.string().min(1),
args: z.array(z.coerce.string().check(z.minLength(1))).min(1),
env: z.looseObject({}),
cwd:z.string().min(1)
}),
active:z.coerce.boolean()
}))
const envList = ref<string[]>([])
const form = useForm({
validationSchema: validateSchema
})
const queryCache=useQueryCache()
const { mutate: fetchMCP } = useMutation({
mutation: (data: Parameters<(Parameters<typeof form.handleSubmit>)[0]>[0]) => request({
url: '/mcp/',
method: 'post',
data
}),
onSettled:()=>queryCache.invalidateQueries({key:['mcp']})
})
const open=inject('open',ref(false))
const createMCP = form.handleSubmit(async (value) => {
// console.log(value)
try {
fetchMCP(value)
open.value=false
} catch {
return
}
})
</script>
@@ -1,5 +1,5 @@
<template>
<aside class="[&_[data-state=collapsed]_.title-container]:hidden">
<aside class="[&_[data-state=collapsed]_:is(.title-container,.exist-btn)]:hidden">
<Sidebar collapsible="icon">
<SidebarHeader>
<SidebarMenu>
@@ -51,7 +51,7 @@
</SidebarContent>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem class="flex justify-center">
<SidebarMenuItem class="flex justify-center exist-btn">
<Button
class="flex-[0.7] mb-10"
@click="exit"
+63 -2
View File
@@ -1,5 +1,66 @@
<template>
<section>
<h1>MCP</h1>
<CreateMCP />
<DataTable
:columns="columns"
:data="[]"
/>
</section>
</template>
</template>
<script setup lang="ts">
import { useQuery } from '@pinia/colada'
import request from '@/utils/request'
import { watch, h, provide,ref } from 'vue'
import DataTable from '@/components/DataTable/index.vue'
import CreateMCP from '@/components/CreateMCP/index.vue'
const open=ref(false)
provide('open',open)
const columns = [
{
accessorKey: 'modelId',
header: () => h('div', { class: 'text-left py-4' }, 'Name'),
cell({ row }) {
return h('div', { class: 'text-left py-4' }, row.getValue('modelId'))
}
},
{
accessorKey: 'baseUrl',
header: () => h('div', { class: 'text-left' }, 'Base Url'),
},
{
accessorKey: 'apiKey',
header: () => h('div', { class: 'text-left' }, 'Api Key'),
},
{
accessorKey: 'clientType',
header: () => h('div', { class: 'text-left' }, 'Client Type'),
},
{
accessorKey: 'name',
header: () => h('div', { class: 'text-left' }, 'Name'),
},
{
accessorKey: 'type',
header: () => h('div', { class: 'text-left' }, 'Type'),
},
{
accessorKey: 'control',
header: () => h('div', { class: 'text-center' }, '操作'),
}
]
const { data: mcpData } = useQuery({
key: ['mcp'],
query: () => request({
url: '/mcp/'
})
})
watch(mcpData, () => {
console.log(mcpData.value?.data)
})
</script>
+1 -1
View File
@@ -11,7 +11,7 @@
Model Settings
</CardDescription>
</CardHeader>
<CardContent>
<CardContent class="mt-4">
<FormField
v-slot="{ componentField }"
name="defaultChatModel"