完成 Resources 的支持

This commit is contained in:
huangzhelong.byte 2025-03-29 19:01:41 +08:00
parent 4947a0d2c2
commit d4e6775a47
15 changed files with 517 additions and 23 deletions

View File

@ -69,6 +69,8 @@ class MessageBridge {
this.postMessage = (message) => {
if (this.ws?.readyState === WebSocket.OPEN) {
console.log(message);
this.ws.send(JSON.stringify(message));
}
};
@ -118,10 +120,6 @@ const messageBridge = new MessageBridge();
export function useMessageBridge() {
const bridge = messageBridge;
onUnmounted(() => {
bridge.destroy();
});
return {
postMessage: bridge.postMessage.bind(bridge),
addCommandListener: bridge.addCommandListener.bind(bridge),

View File

@ -1,8 +1,7 @@
import { reactive } from 'vue';
import Resource from './resource/index.vue';
import Chat from './chat/index.vue';
import Resource from './chat/index.vue';
import Prompt from './prompt/index.vue';
import Tool from './tool/index.vue';

View File

@ -1,16 +1,23 @@
<template>
<div class="resource-module">
<h2>资源模块</h2>
<div class="left">
<h2>
<span class="iconfont icon-file"></span>
资源模块
</h2>
<h3><code>resources/templates/list</code></h3>
<ResourceTemplates></ResourceTemplates>
</div>
<div class="right">
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from 'vue';
import ResourceTemplates from './resource-templates.vue';
defineComponent({ name: 'resource' });
</script>
<style scoped>
@ -18,4 +25,14 @@ defineComponent({ name: 'resource' });
padding: 20px;
height: 100%;
}
.resource-module .left {
width: 45%;
}
.resource-module .right {
width: 45%;
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<div class="resource-template-container-scrollbar">
<el-scrollbar height="500px">
<div class="resource-template-container">
<div
class="item"
v-for="template of resourcesManager.templates"
:key="template.name"
>
<span>{{ template.name }}</span>
<span>{{ template.description || '' }}</span>
</div>
</div>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge';
import { CasualRestAPI, ResourceTemplatesListResponse } from '@/hook/type';
import { onMounted, onUnmounted } from 'vue';
import { resourcesManager } from './resources';
const bridge = useMessageBridge();
let cancelListener: undefined | (() => void) = undefined;
function reloadResources() {
bridge.postMessage({
command: 'resources/templates/list'
});
}
onMounted(() => {
cancelListener = bridge.addCommandListener('resources/templates/list', (data: CasualRestAPI<ResourceTemplatesListResponse>) => {
resourcesManager.templates = data.msg.resourceTemplates;
});
reloadResources();
});
onUnmounted(() => {
if (cancelListener) {
cancelListener();
}
});
</script>
<style>
.resource-template-container-scrollbar {
background-color: var(--background);
border-radius: .5em;
}
.resource-template-container {
height: fit-content;
display: flex;
flex-direction: column;
padding: 10px;
}
.resource-template-container > .item {
margin: 3px;
padding: 5px 10px;
border-radius: .3em;
user-select: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
transition: var(--animation-3s);
}
.resource-template-container > .item:hover {
background-color: var(--main-light-color);
transition: var(--animation-3s);
}
.resource-template-container > .item > span:first-child {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.resource-template-container > .item > span:last-child {
opacity: 0.6;
font-size: 12.5px;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@ -0,0 +1,11 @@
import { ResourceTemplate, ResourceTemplatesListResponse } from '@/hook/type';
import { reactive } from 'vue';
export const resourcesManager = reactive<{
current: ResourceTemplate | undefined
templates: ResourceTemplate[]
}>({
current: undefined,
templates: []
});

View File

@ -30,8 +30,6 @@ function isActive(name: string) {
}
function gotoOption(ident: string) {
console.log(router);
router.push('/' + ident);
}

110
app/src/hook/type.ts Normal file
View File

@ -0,0 +1,110 @@
// ==================== 基础类型定义 ====================
export interface SchemaProperty {
title: string;
type: string;
}
export interface InputSchema {
type: string;
properties: Record<string, SchemaProperty>;
required?: string[];
title?: string;
}
export interface Argument {
name: string;
required: boolean;
}
export interface Content {
uri: string;
mimeType: string;
text: string;
}
export interface MessageContent {
type: string;
text: string;
}
export interface CasualRestAPI<T> {
code: number
msg: T
}
// ==================== 响应接口定义 ====================
export interface ToolsListResponse {
tools: Array<{
name: string;
description: string;
inputSchema: InputSchema;
}>;
}
export interface PromptsListResponse {
prompts: Array<{
name: string;
description: string;
arguments: Argument[];
}>;
}
export interface ResourceTemplate {
uriTemplate: string;
name: string;
description: string;
}
export interface ResourceTemplatesListResponse {
resourceTemplates: ResourceTemplate[]
}
export interface ResourcesListResponse {
resources: any[]; // 根据示例返回空数组,可进一步定义具体类型
}
export interface ResourcesReadResponse {
contents: Content[];
}
export interface PromptsGetResponse {
messages: Array<{
role: string;
content: MessageContent;
}>;
}
// ==================== 请求接口定义 ====================
export interface BaseRequest {
method: string;
params: Record<string, any>;
}
export interface ResourcesReadRequest extends BaseRequest {
method: 'resources/read';
params: {
uri: string;
};
}
export interface PromptsGetRequest extends BaseRequest {
method: 'prompts/get';
params: {
name: string;
arguments: Record<string, any>;
};
}
// ==================== 合并类型定义 ====================
export type APIResponse =
| ToolsListResponse
| PromptsListResponse
| ResourceTemplatesListResponse
| ResourcesListResponse
| ResourcesReadResponse
| PromptsGetResponse;
export type APIRequest =
| BaseRequest
| ResourcesReadRequest
| PromptsGetRequest;

View File

@ -36,7 +36,7 @@ const { t } = useI18n();
user-select: text;
cursor: text;
font-size: 15px;
line-height: 1.3;
line-height: 1.5;
background-color: var(--sidebar);
}
</style>

View File

@ -1,7 +1,16 @@
<template>
<div style="height: 100%;">
<Welcome v-if="!tabs.activeTab.component"></Welcome>
<component v-else :is="tabs.activeTab.component" />
<Welcome v-show="!tabs.activeTab.component"></Welcome>
<!-- 如果存在激活标签页则根据标签页进行渲染 -->
<div v-show="tabs.activeTab.component">
<component
v-for="(tab, index) of tabs.content"
v-show="tab === tabs.activeTab"
:key="index"
:is="tab.component"
/>
</div>
</div>
</template>
@ -11,7 +20,7 @@ import { defineComponent } from 'vue';
import Welcome from './welcome.vue';
import { tabs } from '@/components/main-panel/panel';
defineComponent({ name: 'TEMPLATE_NAME' });
defineComponent({ name: 'debug' });
</script>
<style>

View File

@ -25,3 +25,90 @@ def get_greeting(name: str) -> str:
)
def translate(message: str) -> str:
return f'请将下面的话语翻译成中文:\n\n{message}'
@mcp.tool(
name='multiply',
description='对两个数字进行实数域的乘法运算'
)
def multiply(a: float, b: float) -> float:
"""返回 a 和 b 的乘积"""
return a * b
@mcp.tool(
name='is_even',
description='判断一个整数是否为偶数'
)
def is_even(number: int) -> bool:
"""返回 True 如果数字是偶数,否则 False"""
return number % 2 == 0
@mcp.tool(
name='capitalize',
description='将字符串首字母大写'
)
def capitalize(text: str) -> str:
"""返回首字母大写的字符串"""
return text.capitalize()
@mcp.resource(
uri="weather://{city}",
name='weather',
description='获取指定城市的天气信息'
)
def get_weather(city: str) -> str:
"""模拟天气查询协议,返回格式化字符串"""
return f"Weather in {city}: Sunny, 25°C"
@mcp.resource(
uri="user://{user_id}",
name='user_profile',
description='获取用户基本信息'
)
def get_user_profile(user_id: str) -> dict:
"""模拟用户协议,返回字典数据"""
return {
"id": user_id,
"name": "张三",
"role": "developer"
}
@mcp.resource(
uri="book://{isbn}",
name='book_info',
description='通过ISBN查询书籍信息'
)
def get_book_info(isbn: str) -> dict:
"""模拟书籍协议,返回结构化数据"""
return {
"isbn": isbn,
"title": "Python编程从入门到实践",
"author": "Eric Matthes"
}
@mcp.prompt(
name='summarize',
description='生成文本摘要的提示词模板'
)
def summarize(text: str) -> str:
"""返回摘要生成提示词"""
return f"请用一句话总结以下内容:\n\n{text}"
@mcp.prompt(
name='code_explanation',
description='解释代码功能的提示词模板'
)
def explain_code(code: str) -> str:
"""返回代码解释提示词"""
return f"请解释以下代码的功能:\n```python\n{code}\n```"
@mcp.prompt(
name='email_generator',
description='生成正式邮件的提示词模板'
)
def generate_email(context: str) -> str:
"""返回邮件生成提示词"""
return (
"根据以下需求撰写一封正式邮件:\n"
f"需求描述:{context}\n"
"要求使用礼貌用语长度不超过200字"
)

View File

@ -91,6 +91,11 @@ export class MCPClient {
return await this.client.listResources();
}
// 列出所有模板资源
public async listResourceTemplates() {
return await this.client.listResourceTemplates();
}
// 读取资源
public async readResource(uri: string) {
return await this.client.readResource({

View File

@ -115,6 +115,40 @@ export async function listResources(
}
}
/**
* @description resources
*/
export async function listResourceTemplates(
client: MCPClient | undefined,
webview: VSCodeWebViewLike
) {
if (!client) {
const connectResult = {
code: 501,
msg: 'mcp client 尚未连接'
};
webview.postMessage({ command: 'resources/templates/list', data: connectResult });
return;
}
try {
const resources = await client.listResourceTemplates();
const result = {
code: 200,
msg: resources
};
webview.postMessage({ command: 'resources/templates/list', data: result });
} catch (error) {
const result = {
code: 500,
msg: (error as any).toString()
};
webview.postMessage({ command: 'resources/templates/list', data: result });
}
}
/**
* @description resource
*/

View File

@ -1,7 +1,8 @@
import { VSCodeWebViewLike } from '../adapter';
import { connect, MCPClient, type MCPOptions } from './connect';
import { callTool, getPrompt, listPrompts, listResources, readResource } from './handler';
import { callTool, getPrompt, listPrompts, listResources, listResourceTemplates, readResource } from './handler';
import { ping } from './util';
// TODO: 支持更多的 client
@ -48,6 +49,10 @@ export function messageController(command: string, data: any, webview: VSCodeWeb
listResources(client, webview);
break;
case 'resources/templates/list':
listResourceTemplates(client, webview);
break;
case 'resources/read':
readResource(client, data, webview);
break;
@ -56,6 +61,10 @@ export function messageController(command: string, data: any, webview: VSCodeWeb
callTool(client, data, webview);
break;
case 'ping':
ping(client, webview);
break;
default:
break;
}

View File

@ -0,0 +1,103 @@
// ==================== 基础类型定义 ====================
export interface SchemaProperty {
title: string;
type: string;
}
export interface InputSchema {
type: string;
properties: Record<string, SchemaProperty>;
required?: string[];
title?: string;
}
export interface Argument {
name: string;
required: boolean;
}
export interface Content {
uri: string;
mimeType: string;
text: string;
}
export interface MessageContent {
type: string;
text: string;
}
// ==================== 响应接口定义 ====================
export interface ToolsListResponse {
tools: Array<{
name: string;
description: string;
inputSchema: InputSchema;
}>;
}
export interface PromptsListResponse {
prompts: Array<{
name: string;
description: string;
arguments: Argument[];
}>;
}
export interface ResourceTemplatesListResponse {
resourceTemplates: Array<{
uriTemplate: string;
name: string;
description: string;
}>;
}
export interface ResourcesListResponse {
resources: any[]; // 根据示例返回空数组,可进一步定义具体类型
}
export interface ResourcesReadResponse {
contents: Content[];
}
export interface PromptsGetResponse {
messages: Array<{
role: string;
content: MessageContent;
}>;
}
// ==================== 请求接口定义 ====================
export interface BaseRequest {
method: string;
params: Record<string, any>;
}
export interface ResourcesReadRequest extends BaseRequest {
method: 'resources/read';
params: {
uri: string;
};
}
export interface PromptsGetRequest extends BaseRequest {
method: 'prompts/get';
params: {
name: string;
arguments: Record<string, any>;
};
}
// ==================== 合并类型定义 ====================
export type APIResponse =
| ToolsListResponse
| PromptsListResponse
| ResourceTemplatesListResponse
| ResourcesListResponse
| ResourcesReadResponse
| PromptsGetResponse;
export type APIRequest =
| BaseRequest
| ResourcesReadRequest
| PromptsGetRequest;

View File

@ -0,0 +1,20 @@
import { VSCodeWebViewLike } from "../adapter";
import { MCPClient } from "./connect";
export function ping(client: MCPClient | undefined, webview: VSCodeWebViewLike) {
if (!client) {
const connectResult = {
code: 501,
msg: 'mcp client 尚未连接'
};
webview.postMessage({ command: 'ping', data: connectResult });
return;
}
webview.postMessage({
command: 'ping', data: {
code: 200,
msg: {}
}
});
}