Compare commits
8 Commits
775fadf3d2
...
106c40b128
Author | SHA1 | Date | |
---|---|---|---|
106c40b128 | |||
014e34ea29 | |||
0f80f71e58 | |||
7a2d9d99b9 | |||
a6115d0733 | |||
160ca6dfd1 | |||
d89ba35c55 | |||
![]() |
0653ab4350 |
@ -1,31 +1,176 @@
|
|||||||
import { defineConfig } from 'vitepress'
|
import { defineConfig } from 'vitepress';
|
||||||
|
|
||||||
|
export const customIcons = {
|
||||||
|
share: {
|
||||||
|
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"/></svg>'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// https://vitepress.dev/reference/site-config
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
title: "OpenMCP",
|
title: "OpenMCP",
|
||||||
description: "为开发者和科研人员准备的MCP开发环境和SDK",
|
description: "为开发者和科研人员准备的MCP开发环境和SDK",
|
||||||
themeConfig: {
|
base: '/openmcp',
|
||||||
// https://vitepress.dev/reference/default-theme-config
|
ignoreDeadLinks: true,
|
||||||
nav: [
|
|
||||||
{ text: 'Home', link: '/' },
|
|
||||||
{ text: 'Examples', link: '/markdown-examples' }
|
|
||||||
],
|
|
||||||
|
|
||||||
sidebar: [
|
head: [
|
||||||
{
|
['link', { rel: 'icon', href: '/images/favicon.png' }]
|
||||||
text: 'Examples',
|
],
|
||||||
items: [
|
themeConfig: {
|
||||||
{ text: 'Markdown Examples', link: '/markdown-examples' },
|
// https://vitepress.dev/reference/default-theme-config
|
||||||
{ text: 'Runtime API Examples', link: '/api-examples' }
|
nav: [
|
||||||
]
|
{ text: '首页', link: '/' },
|
||||||
}
|
{
|
||||||
],
|
text: '教程',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
component: 'KNavItem',
|
||||||
|
props: {
|
||||||
|
title: '简介',
|
||||||
|
description: '关于 mcp 和 openmcp,阁下需要知道的 ...',
|
||||||
|
icon: 'a-yusuan2',
|
||||||
|
link: '/plugin-tutorial/index'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'KNavItem',
|
||||||
|
props: {
|
||||||
|
title: 'OpenMCP 使用手册',
|
||||||
|
description: 'OpenMCP Client 的基本使用',
|
||||||
|
icon: 'a-yusuan2',
|
||||||
|
link: '/plugin-tutorial/usage/connect-mcp'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'KNavItem',
|
||||||
|
props: {
|
||||||
|
title: 'MCP 服务器开发案例',
|
||||||
|
description: '使用不同语言开发的不同模式的 MCP 服务器',
|
||||||
|
icon: 'a-yusuan2',
|
||||||
|
link: '/plugin-tutorial/examples/python-simple-stdio'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'KNavItem',
|
||||||
|
props: {
|
||||||
|
title: 'FAQ',
|
||||||
|
description: '为您答疑解惑,排忧解难',
|
||||||
|
icon: 'a-yusuan2',
|
||||||
|
link: '/plugin-tutorial/faq/help'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ text: 'SDK', link: '/sdk-tutorial' },
|
||||||
|
{
|
||||||
|
text: 'Prewiew 0.1.0',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
component: 'KNavItem',
|
||||||
|
props: {
|
||||||
|
title: '更新日志',
|
||||||
|
description: '查看项目的更新历史记录',
|
||||||
|
icon: 'a-yusuan2',
|
||||||
|
link: '/preview/changelog'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'KNavItem',
|
||||||
|
props: {
|
||||||
|
title: '参与 OpenMCP',
|
||||||
|
description: '了解如何参与 OpenMCP 项目的开发和维护',
|
||||||
|
icon: 'shujuzhongxin',
|
||||||
|
link: '/preview/join'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'KNavItem',
|
||||||
|
props: {
|
||||||
|
title: 'OpenMCP 贡献者列表',
|
||||||
|
description: '关于参与 OpenMCP 的贡献者们',
|
||||||
|
icon: 'heike',
|
||||||
|
link: '/preview/contributors'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'KNavItem',
|
||||||
|
props: {
|
||||||
|
title: '资源频道',
|
||||||
|
description: '获取项目相关的资源和信息',
|
||||||
|
icon: 'xinxiang',
|
||||||
|
link: '/preview/channel'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
socialLinks: [
|
sidebar: {
|
||||||
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' }
|
'/plugin-tutorial/': [
|
||||||
],
|
{
|
||||||
footer: {
|
text: '简介',
|
||||||
message: '缩短LLM到Agent的最后一公里'
|
items: [
|
||||||
}
|
{ text: 'OpenMCP 概述', link: '/plugin-tutorial/index' },
|
||||||
}
|
{ text: '获取 OpenMCP', link: '/plugin-tutorial/acquire-openmcp' },
|
||||||
|
{ text: 'MCP 基础概念', link: '/plugin-tutorial/concept' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "OpenMCP 使用手册",
|
||||||
|
items: [
|
||||||
|
{ text: '连接 mcp 服务器', link: '/plugin-tutorial/usage/connect-mcp' },
|
||||||
|
{ text: '调试 tools, resources 和 prompts', link: '/plugin-tutorial/usage/debug' },
|
||||||
|
{ text: '连接大模型', link: '/plugin-tutorial/usage/connect-llm' },
|
||||||
|
{ text: '用大模型测试您的 mcp', link: '/plugin-tutorial/usage/test-with-llm' },
|
||||||
|
{ text: '分发您的实验结果', link: '/plugin-tutorial/usage/distribute-result' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "MCP 服务器开发案例",
|
||||||
|
items: [
|
||||||
|
{ text: '例子 1. python 实现天气信息 mcp 服务器 (STDIO)', link: '/plugin-tutorial/examples/python-simple-stdio' },
|
||||||
|
{ text: '例子 2. go 实现 neo4j 的只读 mcp 服务器 (SSE)', link: '/plugin-tutorial/examples/go-neo4j-sse' },
|
||||||
|
{ text: '例子 3. java 实现文档数据库的只读 mcp (HTTP)', link: '/plugin-tutorial/examples/java-es-http' },
|
||||||
|
{ text: '例子 4. typescript 实现基于 crawl4ai 的超级网页爬虫 mcp (STDIO)', link: '/plugin-tutorial/examples/typescript-crawl4ai-stdio' },
|
||||||
|
{ text: '例子 5. SSE 在线部署的鉴权器实现', link: '/plugin-tutorial/examples/sse-oauth2' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'FAQ',
|
||||||
|
items: [
|
||||||
|
{ text: '帮助', link: '/plugin-tutorial/faq/help' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
'/sdk-tutorial/': [
|
||||||
|
{
|
||||||
|
text: '简介',
|
||||||
|
items: [
|
||||||
|
{ text: 'openmcp-sdk.js', link: '/sdk-tutorial/' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '基本使用',
|
||||||
|
items: [
|
||||||
|
{ text: '最简单的对话', link: '/sdk-tutorial/usage/greet' },
|
||||||
|
{ text: '任务循环', link: '/sdk-tutorial/usage/task-loop' },
|
||||||
|
{ text: '多服务器连接', link: '/sdk-tutorial/usage/multi-server' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
socialLinks: [
|
||||||
|
{ icon: 'github', link: 'https://github.com/LSTM-Kirigaya/openmcp-client' },
|
||||||
|
{ icon: customIcons.share, link: 'https://kirigaya.cn/home' },
|
||||||
|
],
|
||||||
|
|
||||||
|
footer: {
|
||||||
|
message: '缩短LLM到Agent的最后一公里',
|
||||||
|
copyright: 'OpenMCP All rights reserved'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 左上角的 logo
|
||||||
|
logo: '/images/openmcp.png',
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
43
.vitepress/theme/Layout.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useData } from 'vitepress'
|
||||||
|
import DefaultTheme from 'vitepress/theme'
|
||||||
|
import { nextTick, provide } from 'vue'
|
||||||
|
|
||||||
|
const data = useData()
|
||||||
|
const enableTransitions = () =>
|
||||||
|
'startViewTransition' in document &&
|
||||||
|
window.matchMedia('(prefers-reduced-motion: no-preference)').matches
|
||||||
|
|
||||||
|
const isDark = data.isDark;
|
||||||
|
|
||||||
|
// 移除原有的 transition 相关逻辑
|
||||||
|
provide('toggle-appearance', async () => {
|
||||||
|
if (!enableTransitions()) {
|
||||||
|
data.isDark.value = !data.isDark.value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await document.startViewTransition(async () => {
|
||||||
|
isDark.value = !isDark.value
|
||||||
|
await nextTick()
|
||||||
|
}).ready;
|
||||||
|
|
||||||
|
document.documentElement.animate(
|
||||||
|
{
|
||||||
|
duration: 300,
|
||||||
|
easing: 'ease-in',
|
||||||
|
pseudoElement: `::view-transition-${isDark.value ? 'old' : 'new'}(root)`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DefaultTheme.Layout />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 添加全局过渡效果 */
|
||||||
|
:root {
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
</style>
|
200
.vitepress/theme/components/KTab/index.vue
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
<template>
|
||||||
|
<div class="k-tabs" :style="panelStyle">
|
||||||
|
<div class="k-tabs-tags">
|
||||||
|
<div class="k-tabs-tag-item" v-for="pane of tabsContainer.paneInfos" :key="pane.id"
|
||||||
|
:ref="el => tabsContainer.getPanes(el, pane.id)" @click="tabsContainer.switchLabel(pane.id)"
|
||||||
|
:class="{ 'active-tab': tabsContainer.lastPaneId === pane.id }">
|
||||||
|
<span :class="pane.labelClass"></span>
|
||||||
|
<span>{{ pane.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="k-tabs-content" :ref="el => tabsContainer.panelContainer = el">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, useSlots, provide, nextTick, computed, onMounted } from 'vue';
|
||||||
|
|
||||||
|
type PaneInfo = {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
labelClass: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TabsContainer {
|
||||||
|
paneInfos: PaneInfo[];
|
||||||
|
panes: HTMLElement[];
|
||||||
|
lastPaneId?: number;
|
||||||
|
activeLabel: string;
|
||||||
|
panelContainer?: any;
|
||||||
|
height: string;
|
||||||
|
getPanes: (el: any, id: string | number) => void;
|
||||||
|
switchLabel: (id: number) => void;
|
||||||
|
updateLabels: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slots = useSlots();
|
||||||
|
let maxChildHeight = 0;
|
||||||
|
|
||||||
|
function resizeTab(id: number) {
|
||||||
|
const container = tabsContainer.panelContainer;
|
||||||
|
if (container) {
|
||||||
|
const panels = Array.from(container.children) as HTMLElement[];
|
||||||
|
|
||||||
|
const currentPanel = panels[id];
|
||||||
|
if (currentPanel) {
|
||||||
|
maxChildHeight = Math.max(maxChildHeight, currentPanel.clientHeight);
|
||||||
|
tabsContainer.height = maxChildHeight + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelStyle = computed(() => ({
|
||||||
|
height: tabsContainer.height
|
||||||
|
}));
|
||||||
|
|
||||||
|
const tabsContainer: TabsContainer = reactive({
|
||||||
|
paneInfos: [],
|
||||||
|
panes: [],
|
||||||
|
lastPaneId: 0,
|
||||||
|
activeLabel: '',
|
||||||
|
hoverBar: null,
|
||||||
|
panelContainer: undefined,
|
||||||
|
height: '0',
|
||||||
|
getPanes(el: HTMLElement | null, id: string | number) {
|
||||||
|
if (el) {
|
||||||
|
this.panes[id] = el;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
switchLabel(id: number) {
|
||||||
|
if (this.lastPaneId === id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastPaneId = id;
|
||||||
|
this.activeLabel = this.paneInfos[id]?.label || '';
|
||||||
|
|
||||||
|
const container = tabsContainer.panelContainer;
|
||||||
|
const panels = Array.from(container.children) as HTMLElement[];
|
||||||
|
|
||||||
|
panels.forEach((panel, index) => {
|
||||||
|
console.log('index', index);
|
||||||
|
console.log('id', id);
|
||||||
|
|
||||||
|
panel.style.transition = 'opacity 0.3s ease';
|
||||||
|
if (index === id) {
|
||||||
|
panel.style.display = 'block';
|
||||||
|
setTimeout(() => {
|
||||||
|
panel.style.opacity = '1';
|
||||||
|
}, 150);
|
||||||
|
} else {
|
||||||
|
panel.style.opacity = '0';
|
||||||
|
setTimeout(() => {
|
||||||
|
panel.style.display = 'none';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
resizeTab(id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateLabels() {
|
||||||
|
const defaultChildren = slots.default?.() || [];
|
||||||
|
|
||||||
|
this.paneInfos = [];
|
||||||
|
for (const index in defaultChildren) {
|
||||||
|
const vnode = defaultChildren[index];
|
||||||
|
this.paneInfos.push({
|
||||||
|
id: Number(index),
|
||||||
|
label: vnode.props?.label || '',
|
||||||
|
labelClass: vnode.props?.labelClass || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.paneInfos.length > 0) {
|
||||||
|
this.activeLabel = this.paneInfos[0]?.label || '';
|
||||||
|
nextTick(() => {
|
||||||
|
resizeTab(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tabsContainer.updateLabels();
|
||||||
|
onMounted(() => {
|
||||||
|
if (tabsContainer.panelContainer) {
|
||||||
|
const panels = Array.from(tabsContainer.panelContainer.children) as HTMLElement[];
|
||||||
|
panels.forEach((panel, index) => {
|
||||||
|
panel.style.position = 'absolute';
|
||||||
|
|
||||||
|
if (index != tabsContainer.lastPaneId) {
|
||||||
|
panel.style.display = 'none';
|
||||||
|
panel.style.opacity = '0';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.k-tabs {
|
||||||
|
position: relative;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.k-tabs-tags {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.k-tabs-tag-item {
|
||||||
|
background-color: var(--vp-button-alt-bg);
|
||||||
|
color: white;
|
||||||
|
border-radius: .5em;
|
||||||
|
margin-right: 10px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
display: inline-block;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
|
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.k-tabs-tag-item {
|
||||||
|
padding: 6px 16px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.k-tabs-tag-item:hover {
|
||||||
|
background-color: var(--vp-c-brand-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.k-tabs-tag-item.active-tab {
|
||||||
|
background-color: var(--vp-c-brand-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 414px) {
|
||||||
|
.k-tabs-tags {
|
||||||
|
overflow-x: scroll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-bar {
|
||||||
|
background-color: var(--vp-c-brand-3);
|
||||||
|
border-radius: .9em .9em 0 0;
|
||||||
|
transition: .35s ease-in-out;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.k-tabs-content>* {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
</style>
|
105
.vitepress/theme/components/bilibli-player/index.vue
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="bilibili-player-container">
|
||||||
|
<iframe v-if="isPlaying" :src="playerUrl" frameborder="0" allowfullscreen></iframe>
|
||||||
|
|
||||||
|
<div v-else class="cover-container" @click="playVideo">
|
||||||
|
<img :src="props.cover" class="cover-image" />
|
||||||
|
<button class="play-button">
|
||||||
|
<svg viewBox="0 0 24 24" width="48" height="48">
|
||||||
|
<path fill="currentColor" d="M8 5v14l11-7z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
cover: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const isPlaying = ref(false);
|
||||||
|
const playerUrl = ref(props.url);
|
||||||
|
|
||||||
|
function playVideo() {
|
||||||
|
isPlaying.value = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.wrapper {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bilibili-player-container {
|
||||||
|
position: relative;
|
||||||
|
min-width: 377px;
|
||||||
|
min-height: 225px;
|
||||||
|
width: 52.36vw;
|
||||||
|
height: 28.26vw;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
border-radius: .5em;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid var(--vp-c-brand-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--vp-c-brand-3);
|
||||||
|
border: none;
|
||||||
|
color: var(--vp-c-bg);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
transition: transform 0.2s ease, background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button:hover {
|
||||||
|
transform: translate(-50%, -50%) scale(1.1);
|
||||||
|
background-color: var(--vp-c-brand-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button svg {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
39
.vitepress/theme/components/home/HeroImage.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<div style="height: 420px;">
|
||||||
|
<img class="VPImage image-src" src="/images/openmcp.png" alt="">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.VPHero .VPImage {
|
||||||
|
filter: drop-shadow(-2px 4px 6px rgba(0, 0, 0, .2));
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
.image-src {
|
||||||
|
max-width: 320px;
|
||||||
|
max-height: 320px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.image-src {
|
||||||
|
max-width: 256px;
|
||||||
|
max-height: 256px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-src {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
max-width: 350px;
|
||||||
|
max-height: 350px;
|
||||||
|
height: 99%;
|
||||||
|
object-fit: contain;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
106
.vitepress/theme/components/home/TwoSideLayout.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<div class="two-side-layout">
|
||||||
|
<div :class="['two-side-content-container', { 'reverse': imagePlacement === 'left' }]">
|
||||||
|
<div class="text-content">
|
||||||
|
<div v-for="(item, index) in texts" :key="index" class="text-item">
|
||||||
|
<span class="prefix">{{ index + 1 }}.</span> {{ item }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-container">
|
||||||
|
<img :src="image" alt="双栏布局图片" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
imagePlacement: {
|
||||||
|
type: String,
|
||||||
|
default: 'left',
|
||||||
|
validator: (value) => ['left', 'right'].includes(value)
|
||||||
|
},
|
||||||
|
texts: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
labelClass: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.two-side-layout {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-side-content-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-side-content-container.reverse {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-content {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-item {
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 1px solid var(--vp-c-default-3);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prefix {
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
flex: 2;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container img {
|
||||||
|
max-width: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.two-side-content-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-side-content-container.reverse {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
76
.vitepress/theme/components/nav-item/index.vue
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<a class="nav-item" @mouseenter="isHovered = true" @mouseleave="isHovered = false"
|
||||||
|
:href="link"
|
||||||
|
>
|
||||||
|
<div class="nav-item-content">
|
||||||
|
<div class="nav-item-icon" v-if="icon">
|
||||||
|
<span :class=iconClass></span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item-text">
|
||||||
|
<h3 class="nav-item-title">{{ title }}</h3>
|
||||||
|
<p class="nav-item-description">{{ description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: { type: String, required: true },
|
||||||
|
description: { type: String, required: true },
|
||||||
|
icon: { type: String, required: true },
|
||||||
|
link: { type: String, required: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconClass = computed(() => {
|
||||||
|
return `iconfont icon-${props.icon}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isHovered = ref(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
min-width: 300px;
|
||||||
|
padding: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
border-radius: .5em;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
max-width: 20px;
|
||||||
|
max-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item-title {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item-description {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: .7;
|
||||||
|
margin: 0;
|
||||||
|
word-wrap: break-word; /* 确保长单词也能换行 */
|
||||||
|
white-space: normal; /* 允许文本换行 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background-color: var(--vp-c-default-soft);
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
91
.vitepress/theme/iconfont.css
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "iconfont"; /* Project id 4933953 */
|
||||||
|
src: url('iconfont.woff2?t=1748343115691') format('woff2'),
|
||||||
|
url('iconfont.woff?t=1748343115691') format('woff'),
|
||||||
|
url('iconfont.ttf?t=1748343115691') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
font-family: "iconfont" !important;
|
||||||
|
|
||||||
|
font-style: normal;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-heike:before {
|
||||||
|
content: "\e6c5";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-bumendongtai:before {
|
||||||
|
content: "\e61f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-duzhengyishi:before {
|
||||||
|
content: "\e620";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-lianwangzhongxin:before {
|
||||||
|
content: "\e621";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-fenxitongji:before {
|
||||||
|
content: "\e622";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-shujuzhongxin:before {
|
||||||
|
content: "\e623";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-shuju:before {
|
||||||
|
content: "\e624";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-shenji:before {
|
||||||
|
content: "\e625";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-yusuan:before {
|
||||||
|
content: "\e626";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-yibangonggongyusuan:before {
|
||||||
|
content: "\e627";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-xinxiang:before {
|
||||||
|
content: "\e628";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-yujing:before {
|
||||||
|
content: "\e629";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-yijianchuli:before {
|
||||||
|
content: "\e62a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-zhuanti:before {
|
||||||
|
content: "\e62b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-a-yusuan2:before {
|
||||||
|
content: "\e62c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-yujuesuanshencha:before {
|
||||||
|
content: "\e62d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-zhengcefagui:before {
|
||||||
|
content: "\e62e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-ziliao:before {
|
||||||
|
content: "\e62f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-zixuntousu:before {
|
||||||
|
content: "\e630";
|
||||||
|
}
|
||||||
|
|
BIN
.vitepress/theme/iconfont.woff2
Normal file
30
.vitepress/theme/index.mts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// https://vitepress.dev/guide/custom-theme
|
||||||
|
import { h } from 'vue';
|
||||||
|
import type { Theme } from 'vitepress';
|
||||||
|
import DefaultTheme from 'vitepress/theme';
|
||||||
|
|
||||||
|
import CustomLayout from './Layout.vue';
|
||||||
|
|
||||||
|
import HeroImage from './components/home/HeroImage.vue';
|
||||||
|
import TwoSideLayout from './components/home/TwoSideLayout.vue';
|
||||||
|
import KTab from './components/KTab/index.vue';
|
||||||
|
import BiliPlayer from './components/bilibli-player/index.vue';
|
||||||
|
import KNavItem from './components/nav-item/index.vue';
|
||||||
|
|
||||||
|
import './style.css';
|
||||||
|
import './iconfont.css';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: DefaultTheme,
|
||||||
|
Layout: () => {
|
||||||
|
return h(CustomLayout, null, {
|
||||||
|
'home-hero-image': () => h(HeroImage)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
enhanceApp({ app, router, siteData }) {
|
||||||
|
app.component('TwoSideLayout', TwoSideLayout);
|
||||||
|
app.component('KTab', KTab);
|
||||||
|
app.component('BiliPlayer', BiliPlayer);
|
||||||
|
app.component('KNavItem', KNavItem);
|
||||||
|
}
|
||||||
|
} satisfies Theme
|
@ -1,17 +0,0 @@
|
|||||||
// https://vitepress.dev/guide/custom-theme
|
|
||||||
import { h } from 'vue'
|
|
||||||
import type { Theme } from 'vitepress'
|
|
||||||
import DefaultTheme from 'vitepress/theme'
|
|
||||||
import './style.css'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
extends: DefaultTheme,
|
|
||||||
Layout: () => {
|
|
||||||
return h(DefaultTheme.Layout, null, {
|
|
||||||
// https://vitepress.dev/guide/extending-default-theme#layout-slots
|
|
||||||
})
|
|
||||||
},
|
|
||||||
enhanceApp({ app, router, siteData }) {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
} satisfies Theme
|
|
@ -44,30 +44,30 @@
|
|||||||
* -------------------------------------------------------------------------- */
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--vp-c-default-1: var(--vp-c-gray-1);
|
--vp-c-default-1: var(--vp-c-gray-1);
|
||||||
--vp-c-default-2: var(--vp-c-gray-2);
|
--vp-c-default-2: var(--vp-c-gray-2);
|
||||||
--vp-c-default-3: var(--vp-c-gray-3);
|
--vp-c-default-3: var(--vp-c-gray-3);
|
||||||
--vp-c-default-soft: var(--vp-c-gray-soft);
|
--vp-c-default-soft: var(--vp-c-gray-soft);
|
||||||
|
|
||||||
--vp-c-brand-1: var(--vp-c-indigo-1);
|
--vp-c-brand-1: #d8a8e7; /* 较深的变体 */
|
||||||
--vp-c-brand-2: var(--vp-c-indigo-2);
|
--vp-c-brand-2: #C285D6; /* 基底颜色 */
|
||||||
--vp-c-brand-3: var(--vp-c-indigo-3);
|
--vp-c-brand-3: #a368b8; /* 较浅的变体 */
|
||||||
--vp-c-brand-soft: var(--vp-c-indigo-soft);
|
--vp-c-brand-soft: rgba(194, 133, 214, 0.1); /* 半透明的柔和版本 */
|
||||||
|
|
||||||
--vp-c-tip-1: var(--vp-c-brand-1);
|
--vp-c-tip-1: var(--vp-c-brand-1);
|
||||||
--vp-c-tip-2: var(--vp-c-brand-2);
|
--vp-c-tip-2: var(--vp-c-brand-2);
|
||||||
--vp-c-tip-3: var(--vp-c-brand-3);
|
--vp-c-tip-3: var(--vp-c-brand-3);
|
||||||
--vp-c-tip-soft: var(--vp-c-brand-soft);
|
--vp-c-tip-soft: var(--vp-c-brand-soft);
|
||||||
|
|
||||||
--vp-c-warning-1: var(--vp-c-yellow-1);
|
--vp-c-warning-1: var(--vp-c-yellow-1);
|
||||||
--vp-c-warning-2: var(--vp-c-yellow-2);
|
--vp-c-warning-2: var(--vp-c-yellow-2);
|
||||||
--vp-c-warning-3: var(--vp-c-yellow-3);
|
--vp-c-warning-3: var(--vp-c-yellow-3);
|
||||||
--vp-c-warning-soft: var(--vp-c-yellow-soft);
|
--vp-c-warning-soft: var(--vp-c-yellow-soft);
|
||||||
|
|
||||||
--vp-c-danger-1: var(--vp-c-red-1);
|
--vp-c-danger-1: var(--vp-c-red-1);
|
||||||
--vp-c-danger-2: var(--vp-c-red-2);
|
--vp-c-danger-2: var(--vp-c-red-2);
|
||||||
--vp-c-danger-3: var(--vp-c-red-3);
|
--vp-c-danger-3: var(--vp-c-red-3);
|
||||||
--vp-c-danger-soft: var(--vp-c-red-soft);
|
--vp-c-danger-soft: var(--vp-c-red-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -75,15 +75,15 @@
|
|||||||
* -------------------------------------------------------------------------- */
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--vp-button-brand-border: transparent;
|
--vp-button-brand-border: transparent;
|
||||||
--vp-button-brand-text: var(--vp-c-white);
|
--vp-button-brand-text: var(--vp-c-white);
|
||||||
--vp-button-brand-bg: var(--vp-c-brand-3);
|
--vp-button-brand-bg: var(--vp-c-brand-3);
|
||||||
--vp-button-brand-hover-border: transparent;
|
--vp-button-brand-hover-border: transparent;
|
||||||
--vp-button-brand-hover-text: var(--vp-c-white);
|
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||||
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
|
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
|
||||||
--vp-button-brand-active-border: transparent;
|
--vp-button-brand-active-border: transparent;
|
||||||
--vp-button-brand-active-text: var(--vp-c-white);
|
--vp-button-brand-active-text: var(--vp-c-white);
|
||||||
--vp-button-brand-active-bg: var(--vp-c-brand-1);
|
--vp-button-brand-active-bg: var(--vp-c-brand-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,31 +91,27 @@
|
|||||||
* -------------------------------------------------------------------------- */
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--vp-home-hero-name-color: transparent;
|
--vp-home-hero-name-color: transparent;
|
||||||
--vp-home-hero-name-background: -webkit-linear-gradient(
|
--vp-home-hero-name-background: -webkit-linear-gradient(120deg,
|
||||||
120deg,
|
#bd34fe 30%,
|
||||||
#bd34fe 30%,
|
#41d1ff);
|
||||||
#41d1ff
|
|
||||||
);
|
|
||||||
|
|
||||||
--vp-home-hero-image-background-image: linear-gradient(
|
--vp-home-hero-image-background-image: linear-gradient(-45deg,
|
||||||
-45deg,
|
#bd34fe 50%,
|
||||||
#bd34fe 50%,
|
#47caff 50%);
|
||||||
#47caff 50%
|
--vp-home-hero-image-filter: blur(44px);
|
||||||
);
|
|
||||||
--vp-home-hero-image-filter: blur(44px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
:root {
|
:root {
|
||||||
--vp-home-hero-image-filter: blur(56px);
|
--vp-home-hero-image-filter: blur(56px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 960px) {
|
@media (min-width: 960px) {
|
||||||
:root {
|
:root {
|
||||||
--vp-home-hero-image-filter: blur(68px);
|
--vp-home-hero-image-filter: blur(68px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -123,10 +119,15 @@
|
|||||||
* -------------------------------------------------------------------------- */
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--vp-custom-block-tip-border: transparent;
|
--vp-custom-block-tip-border: transparent;
|
||||||
--vp-custom-block-tip-text: var(--vp-c-text-1);
|
--vp-custom-block-tip-text: var(--vp-c-text-1);
|
||||||
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
|
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
|
||||||
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
|
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-home-hero-name-color: transparent;
|
||||||
|
--vp-home-hero-name-background: -webkit-linear-gradient(120deg, #bd34fe, #41d1ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -134,6 +135,59 @@
|
|||||||
* -------------------------------------------------------------------------- */
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
.DocSearch {
|
.DocSearch {
|
||||||
--docsearch-primary-color: var(--vp-c-brand-1) !important;
|
--docsearch-primary-color: var(--vp-c-brand-1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.two-side-layout .image-container {
|
||||||
|
display: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VPHero .image-container img {
|
||||||
|
box-shadow: unset !important;
|
||||||
|
max-width: 290px !important;
|
||||||
|
max-height: 290px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
border: 2px solid var(--vp-c-brand-3);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.VPMenu {
|
||||||
|
background-color: rgba(255, 255, 255, 0.55) !important;
|
||||||
|
border: unset !important;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.dark .VPMenu {
|
||||||
|
background-color: rgba(0, 0, 0, 0.55) !important;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.VPTeamPage {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义全局滚动条样式 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--vp-c-default-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--vp-c-brand-3);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--vp-c-brand-2);
|
||||||
|
}
|
20
.vscode/vue.code-snippets
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"Vue 3 TypeScript Component": {
|
||||||
|
"prefix": "vue3ts",
|
||||||
|
"body": [
|
||||||
|
"<template>",
|
||||||
|
"\t$1",
|
||||||
|
"</template>",
|
||||||
|
"",
|
||||||
|
"<script setup lang=\"ts\">",
|
||||||
|
"import { defineComponent } from 'vue';",
|
||||||
|
"",
|
||||||
|
"defineComponent({ name: '$TEMPLATE_NAME' });",
|
||||||
|
"</script>",
|
||||||
|
"",
|
||||||
|
"<style>",
|
||||||
|
"</style>"
|
||||||
|
],
|
||||||
|
"description": "Vue 3 TypeScript component template"
|
||||||
|
}
|
||||||
|
}
|
88
README.md
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# OpenMCP Document
|
||||||
|
|
||||||
|
## 📚 项目简介
|
||||||
|
OpenMCP 是基于 MCP 协议(Multi-Model Communication Protocol)开发的开源框架,由开发者 **LSTM-Kirigaya** 创建。该项目通过深度整合 DeepSeek 等大模型 API,提供了一套完整的工具链,支持开发者通过自然语言与数据库(如 Neo4j)、本地资源及外部工具交互,实现自动化开发、资讯聚合等场景。项目文档仓库包含技术原理、开发指南及示例代码。
|
||||||
|
|
||||||
|
## 🌟 核心特性
|
||||||
|
|
||||||
|
### 1. **MCP 协议支持**
|
||||||
|
- **Resources**:支持访问本地文件系统、数据库等静态资源,扩展大模型上下文。
|
||||||
|
- **Prompts**:提供场景化 Prompt 模板,引导大模型生成结构化输出。
|
||||||
|
- **Tools**:封装函数工具(如酒店预订、网页操作),通过 Function Calling 实现 AI 与现实世界的交互。
|
||||||
|
|
||||||
|
### 2. **低成本高效开发**
|
||||||
|
- 深度集成 **DeepSeek API**,Token 消耗极低(开发者实例:月均费用 19 元,对比 OpenAI API 成本降低 70%+)。
|
||||||
|
- 支持凌晨调用 API 享受更低价格,降低个人开发者门槛。
|
||||||
|
|
||||||
|
### 3. **典型应用场景**
|
||||||
|
- **自动化开发**:通过自然语言生成代码(如后端服务、Neo4j 交互)。
|
||||||
|
- **资讯机器人**:定时推送最新资讯(示例:每日 10 点群发技术动态)。
|
||||||
|
- **AI 工具链**:快速构建 AI 驱动的工具(如数据分析、智能客服)。
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 1. 环境准备
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
pip install numpy opencv-python tqdm # 示例依赖,具体见项目文档
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 核心功能示例
|
||||||
|
```python
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
# 初始化 DeepSeek 客户端
|
||||||
|
client = OpenAI(
|
||||||
|
api_key="YOUR_DEEPSEEK_KEY",
|
||||||
|
base_url="https://api.deepseek.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 调用大模型处理任务
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="deepseek-chat",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "你是一个自动化开发助手"},
|
||||||
|
{"role": "user", "content": "用 Python 写一个连接 Neo4j 的 CRUD 接口"}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
print(response.choices[0].message.content)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ 开发贡献
|
||||||
|
|
||||||
|
### 1. 本地开发流程
|
||||||
|
1. Fork 仓库并克隆:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/YOUR_USERNAME/openmcp-document.git
|
||||||
|
```
|
||||||
|
2. 安装开发依赖:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
3. 提交代码并推送 PR。
|
||||||
|
|
||||||
|
### 2. 打包发布(针对核心库)
|
||||||
|
```bash
|
||||||
|
# 生成发布包
|
||||||
|
python -m build
|
||||||
|
# 上传至 PyPI
|
||||||
|
twine upload dist/*
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📞 交流社区
|
||||||
|
- **QQ 群**:782833642(OpenMCP 技术交流)
|
||||||
|
- **开发者博客**:[Kirigaya 技术专栏](https://zhuanlan.zhihu.com/kirigaya)
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
本项目采用 **Apache 2.0** 许可证,允许自由使用、修改及商业发布(需保留版权声明)。
|
||||||
|
|
||||||
|
## 🙏 致谢
|
||||||
|
- 感谢 **DeepSeek** 提供低成本、高性能的大模型 API。
|
||||||
|
- 感谢所有贡献者及社区用户的反馈与支持!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Star 历史**
|
||||||
|

|
||||||
|
👉 [立即体验 OpenMCP](https://github.com/LSTM-Kirigaya/openmcp-document) | [文档地址](https://github.com/LSTM-Kirigaya/openmcp-document/wiki)
|
@ -1,49 +0,0 @@
|
|||||||
---
|
|
||||||
outline: deep
|
|
||||||
---
|
|
||||||
|
|
||||||
# Runtime API Examples
|
|
||||||
|
|
||||||
This page demonstrates usage of some of the runtime APIs provided by VitePress.
|
|
||||||
|
|
||||||
The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files:
|
|
||||||
|
|
||||||
```md
|
|
||||||
<script setup>
|
|
||||||
import { useData } from 'vitepress'
|
|
||||||
|
|
||||||
const { theme, page, frontmatter } = useData()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
## Results
|
|
||||||
|
|
||||||
### Theme Data
|
|
||||||
<pre>{{ theme }}</pre>
|
|
||||||
|
|
||||||
### Page Data
|
|
||||||
<pre>{{ page }}</pre>
|
|
||||||
|
|
||||||
### Page Frontmatter
|
|
||||||
<pre>{{ frontmatter }}</pre>
|
|
||||||
```
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { useData } from 'vitepress'
|
|
||||||
|
|
||||||
const { site, theme, page, frontmatter } = useData()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
## Results
|
|
||||||
|
|
||||||
### Theme Data
|
|
||||||
<pre>{{ theme }}</pre>
|
|
||||||
|
|
||||||
### Page Data
|
|
||||||
<pre>{{ page }}</pre>
|
|
||||||
|
|
||||||
### Page Frontmatter
|
|
||||||
<pre>{{ frontmatter }}</pre>
|
|
||||||
|
|
||||||
## More
|
|
||||||
|
|
||||||
Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).
|
|
BIN
images/favicon.png
Normal file
After Width: | Height: | Size: 72 KiB |
34
images/icons/openmcp-sdk.svg
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg width="600" height="674" viewBox="0 0 600 674" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gradient_1" gradientUnits="userSpaceOnUse" x1="300" y1="0" x2="300" y2="600">
|
||||||
|
<stop offset="0" stop-color="#A1A7F6" />
|
||||||
|
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0.2" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="gradient_2" gradientUnits="userSpaceOnUse" x1="110.5" y1="0" x2="110.5" y2="221">
|
||||||
|
<stop offset="0.441" stop-color="#A8A3FF" />
|
||||||
|
<stop offset="1" stop-color="#FFFFFF" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="gradient_3" gradientUnits="userSpaceOnUse" x1="55.5" y1="0" x2="55.5" y2="111">
|
||||||
|
<stop offset="0" stop-color="#FFFFFF" />
|
||||||
|
<stop offset="1" stop-color="#A489FB" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="gradient_5" gradientUnits="userSpaceOnUse" x1="126" y1="0" x2="126" y2="647">
|
||||||
|
<stop offset="0" stop-color="#FFF2B0" />
|
||||||
|
<stop offset="0.461" stop-color="#F2B63A" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<g transform="translate(0 74)">
|
||||||
|
<path d="M300 0C465.708 0 600 134.292 600 300C600 300 600 300 600 300C600 465.708 465.708 600 300 600C300 600 300 600 300 600C134.292 600 0 465.708 0 300C0 300 0 300 0 300C0 134.292 134.292 0 300 0Z" fill="#5A00FF" fill-rule="evenodd" />
|
||||||
|
<path d="M300 0C465.708 0 600 134.292 600 300C600 300 600 300 600 300C600 465.708 465.708 600 300 600C300 600 300 600 300 600C134.292 600 0 465.708 0 300C0 300 0 300 0 300C0 134.292 134.292 0 300 0Z" fill="url(#gradient_1)" fill-rule="evenodd" />
|
||||||
|
</g>
|
||||||
|
<path d="M0 110.5C0 49.4725 49.4725 0 110.5 0C171.527 0 221 49.4725 221 110.5C221 171.527 171.527 221 110.5 221C49.4725 221 0 171.527 0 110.5Z" fill="url(#gradient_2)" fill-rule="evenodd" transform="translate(284 417)" />
|
||||||
|
<path d="M0 55.5C0 24.8482 24.8482 0 55.5 0C86.1518 0 111 24.8482 111 55.5C111 86.1518 86.1518 111 55.5 111C24.8482 111 0 86.1518 0 55.5Z" fill="url(#gradient_3)" fill-rule="evenodd" transform="translate(49 374)" />
|
||||||
|
<path d="M0 182.5C0 81.708 81.708 0 182.5 0C283.292 0 365 81.708 365 182.5C365 283.292 283.292 365 182.5 365C81.708 365 0 283.292 0 182.5Z" fill="url(#gradient_4)" fill-rule="evenodd" transform="translate(179 108)" />
|
||||||
|
<path d="M215.354 294.309L24.8044 647C24.8044 647 80.3091 362.484 80.3091 362.484C80.9377 359.199 79.5325 355.85 77.4987 355.85C77.4987 355.85 23.6951 355.85 23.6951 355.85C6.50011 355.85 -4.96321 325.459 2.13664 298.669C2.13664 298.669 75.28 23.6938 75.28 23.6938C79.1258 9.28799 87.5569 0 96.8384 0C96.8384 0 252 0 252 0C252 0 178.82 218.868 178.82 218.868C177.71 222.217 179.115 226.45 181.371 226.45C181.371 226.45 197.938 226.45 197.938 226.45C218.571 226.45 229.332 268.404 215.354 294.309C215.354 294.309 215.354 294.309 215.354 294.309Z" fill="url(#gradient_5)" transform="translate(193 0)" />
|
||||||
|
</g>
|
||||||
|
<rect width="432" height="432" transform="translate(84 107)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
21
images/icons/openmcp.svg
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 824 834" preserveAspectRatio="xMidYMid meet" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gradient_1" gradientUnits="userSpaceOnUse" x1="300" y1="0" x2="300" y2="600">
|
||||||
|
<stop offset="0" stop-color="#A1A7F6" />
|
||||||
|
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0.2" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<g transform="translate(145 58)">
|
||||||
|
<g>
|
||||||
|
<path d="M300 0C465.708 0 600 134.292 600 300C600 300 600 300 600 300C600 465.708 465.708 600 300 600C300 600 300 600 300 600C134.292 600 0 465.708 0 300C0 300 0 300 0 300C0 134.292 134.292 0 300 0Z" fill="#5A00FF" fill-rule="evenodd" />
|
||||||
|
<path d="M300 0C465.708 0 600 134.292 600 300C600 300 600 300 600 300C600 465.708 465.708 600 300 600C300 600 300 600 300 600C134.292 600 0 465.708 0 300C0 300 0 300 0 300C0 134.292 134.292 0 300 0Z" fill="url(#gradient_1)" fill-rule="evenodd" />
|
||||||
|
</g>
|
||||||
|
<path d="M0 110.5C0 49.4725 49.4725 0 110.5 0C171.527 0 221 49.4725 221 110.5C221 171.527 171.527 221 110.5 221C49.4725 221 0 171.527 0 110.5Z" fill="#FFFFFF" fill-rule="evenodd" transform="translate(294 341)" />
|
||||||
|
<path d="M0 55.5C0 24.8482 24.8482 0 55.5 0C86.1518 0 111 24.8482 111 55.5C111 86.1518 86.1518 111 55.5 111C24.8482 111 0 86.1518 0 55.5Z" fill="#FFFFFF" fill-rule="evenodd" transform="translate(48 269)" />
|
||||||
|
<path d="M0 182.5C0 81.708 81.708 0 182.5 0C283.292 0 365 81.708 365 182.5C365 283.292 283.292 365 182.5 365C81.708 365 0 283.292 0 182.5Z" fill="#FFFFFF" fill-rule="evenodd" transform="translate(188 39)" />
|
||||||
|
<path d="M0 57C0 25.5198 25.5198 0 57 0C88.4802 0 114 25.5198 114 57C114 88.4802 88.4802 114 57 114C25.5198 114 0 88.4802 0 57Z" fill="#FFFFFF" fill-rule="evenodd" transform="translate(376 130)" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
41
images/icons/vscode.svg
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.9119 99.3171C72.4869 99.9307 74.2828 99.8914 75.8725 99.1264L96.4608 89.2197C98.6242 88.1787 100 85.9892 100 83.5872V16.4133C100 14.0113 98.6243 11.8218 96.4609 10.7808L75.8725 0.873756C73.7862 -0.130129 71.3446 0.11576 69.5135 1.44695C69.252 1.63711 69.0028 1.84943 68.769 2.08341L29.3551 38.0415L12.1872 25.0096C10.589 23.7965 8.35363 23.8959 6.86933 25.2461L1.36303 30.2549C-0.452552 31.9064 -0.454633 34.7627 1.35853 36.417L16.2471 50.0001L1.35853 63.5832C-0.454633 65.2374 -0.452552 68.0938 1.36303 69.7453L6.86933 74.7541C8.35363 76.1043 10.589 76.2037 12.1872 74.9905L29.3551 61.9587L68.769 97.9167C69.3925 98.5406 70.1246 99.0104 70.9119 99.3171ZM75.0152 27.2989L45.1091 50.0001L75.0152 72.7012V27.2989Z" fill="white"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0)">
|
||||||
|
<path d="M96.4614 10.7962L75.8569 0.875542C73.4719 -0.272773 70.6217 0.211611 68.75 2.08333L1.29858 63.5832C-0.515693 65.2373 -0.513607 68.0937 1.30308 69.7452L6.81272 74.754C8.29793 76.1042 10.5347 76.2036 12.1338 74.9905L93.3609 13.3699C96.086 11.3026 100 13.2462 100 16.6667V16.4275C100 14.0265 98.6246 11.8378 96.4614 10.7962Z" fill="#0065A9"/>
|
||||||
|
<g filter="url(#filter0_d)">
|
||||||
|
<path d="M96.4614 89.2038L75.8569 99.1245C73.4719 100.273 70.6217 99.7884 68.75 97.9167L1.29858 36.4169C-0.515693 34.7627 -0.513607 31.9063 1.30308 30.2548L6.81272 25.246C8.29793 23.8958 10.5347 23.7964 12.1338 25.0095L93.3609 86.6301C96.086 88.6974 100 86.7538 100 83.3334V83.5726C100 85.9735 98.6246 88.1622 96.4614 89.2038Z" fill="#007ACC"/>
|
||||||
|
</g>
|
||||||
|
<g filter="url(#filter1_d)">
|
||||||
|
<path d="M75.8578 99.1263C73.4721 100.274 70.6219 99.7885 68.75 97.9166C71.0564 100.223 75 98.5895 75 95.3278V4.67213C75 1.41039 71.0564 -0.223106 68.75 2.08329C70.6219 0.211402 73.4721 -0.273666 75.8578 0.873633L96.4587 10.7807C98.6234 11.8217 100 14.0112 100 16.4132V83.5871C100 85.9891 98.6234 88.1786 96.4586 89.2196L75.8578 99.1263Z" fill="#1F9CF0"/>
|
||||||
|
</g>
|
||||||
|
<g style="mix-blend-mode:overlay" opacity="0.25">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.8511 99.3171C72.4261 99.9306 74.2221 99.8913 75.8117 99.1264L96.4 89.2197C98.5634 88.1787 99.9392 85.9892 99.9392 83.5871V16.4133C99.9392 14.0112 98.5635 11.8217 96.4001 10.7807L75.8117 0.873695C73.7255 -0.13019 71.2838 0.115699 69.4527 1.44688C69.1912 1.63705 68.942 1.84937 68.7082 2.08335L29.2943 38.0414L12.1264 25.0096C10.5283 23.7964 8.29285 23.8959 6.80855 25.246L1.30225 30.2548C-0.513334 31.9064 -0.515415 34.7627 1.29775 36.4169L16.1863 50L1.29775 63.5832C-0.515415 65.2374 -0.513334 68.0937 1.30225 69.7452L6.80855 74.754C8.29285 76.1042 10.5283 76.2036 12.1264 74.9905L29.2943 61.9586L68.7082 97.9167C69.3317 98.5405 70.0638 99.0104 70.8511 99.3171ZM74.9544 27.2989L45.0483 50L74.9544 72.7012V27.2989Z" fill="url(#paint0_linear)"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d" x="-8.39411" y="15.8291" width="116.727" height="92.2456" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||||
|
<feOffset/>
|
||||||
|
<feGaussianBlur stdDeviation="4.16667"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||||
|
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="filter1_d" x="60.4167" y="-8.07558" width="47.9167" height="116.151" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||||
|
<feOffset/>
|
||||||
|
<feGaussianBlur stdDeviation="4.16667"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||||
|
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<linearGradient id="paint0_linear" x1="49.9392" y1="0.257812" x2="49.9392" y2="99.7423" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white"/>
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.3 KiB |
BIN
images/openmcp-default.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
images/openmcp.chatbot.png
Normal file
After Width: | Height: | Size: 543 KiB |
BIN
images/openmcp.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
images/opensource.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
images/public-deploy.png
Normal file
After Width: | Height: | Size: 431 KiB |
93
index.md
@ -4,22 +4,89 @@ layout: home
|
|||||||
|
|
||||||
hero:
|
hero:
|
||||||
name: "OpenMCP"
|
name: "OpenMCP"
|
||||||
text: "elegant mcp development tools and sdk for developers and researchers"
|
text: "面向开发者的优雅 MCP 调试器和 SDK"
|
||||||
tagline: Shortening the last mile from LLM to Agent
|
tagline: 缩短从大语言模型到智能体的最后一公里
|
||||||
|
image:
|
||||||
|
src: /images/openmcp.png
|
||||||
|
alt: VitePress
|
||||||
|
|
||||||
actions:
|
actions:
|
||||||
- theme: brand
|
- theme: brand
|
||||||
text: Markdown Examples
|
text: OpenMCP 插件
|
||||||
link: /markdown-examples
|
link: /plugin-tutorial
|
||||||
- theme: alt
|
- theme: alt
|
||||||
text: API Examples
|
text: openmcp-sdk
|
||||||
link: /api-examples
|
link: /sdk-tutorial
|
||||||
|
- theme: alt
|
||||||
|
text: GitHub
|
||||||
|
link: https://github.com/LSTM-Kirigaya/openmcp-client
|
||||||
features:
|
features:
|
||||||
- title: Feature A
|
- icon:
|
||||||
details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
|
src: /images/icons/vscode.svg
|
||||||
- title: Feature B
|
height: 48px
|
||||||
details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
|
alt: 集成调试环境
|
||||||
- title: Feature C
|
title: 集成调试环境
|
||||||
details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
|
details: 将检查器与 MCP 客户端功能相结合,实现无缝开发和测试
|
||||||
|
- icon:
|
||||||
|
src: /images/openmcp.png
|
||||||
|
height: 48px
|
||||||
|
alt: 提供完整的项目级控制面板
|
||||||
|
title: 全面的项目管理
|
||||||
|
details: 提供完整的项目级控制面板,实现高效的 MCP 项目监督
|
||||||
|
- icon:
|
||||||
|
src: /images/icons/openmcp-sdk.svg
|
||||||
|
height: 48px
|
||||||
|
alt: 提供完整的项目级控制面板
|
||||||
|
title: 完整的部署方案
|
||||||
|
details: 将测试完成的 agent 通过 openmcp-sdk 部署到您的应用或者服务器上
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<BiliPlayer
|
||||||
|
url="//player.bilibili.com/player.html?isOutside=true&aid=114445745397200&bvid=BV1zYGozgEHcautoplay=false"
|
||||||
|
cover="https://picx.zhimg.com/80/v2-8c1f5d99066ed272554146ed8caf7cc3_1440w.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## OpenMCP 为谁准备?
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<KTab>
|
||||||
|
<TwoSideLayout
|
||||||
|
label="专业软件工程师"
|
||||||
|
:texts="[
|
||||||
|
'测试左移,让你的开发与测试一体化,无需打开第三方软件。提供极其丰富的功能和特性。',
|
||||||
|
'在左侧面板自由而优雅地管理、调试和测试你的智能体。',
|
||||||
|
'大模型调用工具的每一个细节一览无余,不满意的调用结果直接一键复现。',
|
||||||
|
'每一次对话都会显示各项性能指标,方便进行成本管理。',
|
||||||
|
'系统提示词管理面板,让您轻松用 mcp 服务器和系统提示词构建您的智能体应用。',
|
||||||
|
]"
|
||||||
|
image="/images/openmcp.chatbot.png"
|
||||||
|
/>
|
||||||
|
<TwoSideLayout
|
||||||
|
label="开源社区爱好者"
|
||||||
|
:texts="[
|
||||||
|
'测试左移,让你的开发与测试一体化,无需打开第三方软件。提供极其丰富的功能和特性。',
|
||||||
|
'OpenMCP 完全开源,您不仅可以免费试用此产品,也可以一起加入我们,实现你的关于 Agent 的奇思妙想',
|
||||||
|
'完全公开技术细节,您不必担心,您的创意和token会遭到剽窃',
|
||||||
|
'可持久化的系统提示词管理面板,让您可以将实际的 mcp 服务器的系统提示词进行测试,以便于在社区内进行分享',
|
||||||
|
'每一次测试的细节都会 100% 跟随 git 进行版本控制,方便你分享你的每一次试验结果,也方便你零成本复现别人的 mcp 项目。'
|
||||||
|
]"
|
||||||
|
image="/images/opensource.png"
|
||||||
|
/>
|
||||||
|
<TwoSideLayout
|
||||||
|
label="AI研发科学家"
|
||||||
|
:texts="[
|
||||||
|
'测试左移,让你的开发与测试一体化,无需打开第三方软件。提供极其丰富的功能和特性。',
|
||||||
|
'只需几行代码,就能快速将您的科研成果以做成 mcp 服务器,从而接入任意大模型,以实现用户友好型的交互界面。',
|
||||||
|
'所有实验数据与配置参数均自动纳入Git版本管理系统,确保研究成果可追溯、可复现,便于学术交流与论文复现。',
|
||||||
|
'基于 OpenMCP 快速完成您的 demo,缩短创新到落地的距离',
|
||||||
|
]"
|
||||||
|
image="/images/openmcp.chatbot.png"
|
||||||
|
/>
|
||||||
|
</KTab>
|
||||||
|
|
||||||
|
<!-- -->
|
@ -1,85 +0,0 @@
|
|||||||
# Markdown Extension Examples
|
|
||||||
|
|
||||||
This page demonstrates some of the built-in markdown extensions provided by VitePress.
|
|
||||||
|
|
||||||
## Syntax Highlighting
|
|
||||||
|
|
||||||
VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting:
|
|
||||||
|
|
||||||
**Input**
|
|
||||||
|
|
||||||
````md
|
|
||||||
```js{4}
|
|
||||||
export default {
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
msg: 'Highlighted!'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
````
|
|
||||||
|
|
||||||
**Output**
|
|
||||||
|
|
||||||
```js{4}
|
|
||||||
export default {
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
msg: 'Highlighted!'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Custom Containers
|
|
||||||
|
|
||||||
**Input**
|
|
||||||
|
|
||||||
```md
|
|
||||||
::: info
|
|
||||||
This is an info box.
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: tip
|
|
||||||
This is a tip.
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: warning
|
|
||||||
This is a warning.
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: danger
|
|
||||||
This is a dangerous warning.
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: details
|
|
||||||
This is a details block.
|
|
||||||
:::
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output**
|
|
||||||
|
|
||||||
::: info
|
|
||||||
This is an info box.
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: tip
|
|
||||||
This is a tip.
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: warning
|
|
||||||
This is a warning.
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: danger
|
|
||||||
This is a dangerous warning.
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: details
|
|
||||||
This is a details block.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## More
|
|
||||||
|
|
||||||
Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).
|
|
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"docs:dev": "vitepress dev",
|
"dev": "vitepress dev",
|
||||||
"docs:build": "vitepress build",
|
"build": "vitepress build",
|
||||||
"docs:preview": "vitepress preview"
|
"preview": "vitepress preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vitepress": "^1.6.3"
|
"vitepress": "^1.6.3"
|
||||||
|
51
plugin-tutorial/acquire-openmcp.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
layout: doc
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# 获取 OpenMCP
|
||||||
|
|
||||||
|
## 在插件商城中安装 OpenMCP
|
||||||
|
|
||||||
|
你可以在主流 VLE 的插件商城直接获取 OpenMCP 插件。比如在 vscode 中,点击左侧的插件商城,然后在搜索框中输入 `OpenMCP` 即可找到 OpenMCP 插件。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 离线安装
|
||||||
|
|
||||||
|
VLE 的插件本质是一个 zip 压缩包,后缀名为 vsix,全平台通用。我们的 CI/CD 机器人在每次版本发布后,会自动构建并上传 vsix 到 github release,你可以通过如下的链接访问到对应版本的 github release 页面:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://github.com/LSTM-Kirigaya/openmcp-client/releases/tag/v{版本号}
|
||||||
|
```
|
||||||
|
|
||||||
|
比如对于 0.1.1 这个版本,它的 release 页面链接为:[https://github.com/LSTM-Kirigaya/openmcp-client/releases/tag/v0.1.1](https://github.com/LSTM-Kirigaya/openmcp-client/releases/tag/v0.1.1)
|
||||||
|
|
||||||
|
在 `Assets` 下面,你可以找到对应的 vsix 压缩包
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
除此之外,您还可以通过如下的商城网页来获取最新的 openmcp 的 vsix
|
||||||
|
|
||||||
|
- https://open-vsx.org/extension/kirigaya/openmcp
|
||||||
|
- https://marketplace.visualstudio.com/items?itemName=kirigaya.openmcp
|
||||||
|
|
||||||
|
点击 vsix 后缀名的文件下载,下载完成后,您就可以直接安装它了。在 VLE 中安装外部的 vsix 文件有两种方法。
|
||||||
|
|
||||||
|
### 方法一:在 VLE 中安装
|
||||||
|
|
||||||
|
VLE 的插件商城页面有一个三个点的按钮,点击它后,你能看到下面这个列表中被我标红的按钮
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
点击它后,找到刚刚下载的 vsix 文件,点击即可完成安装。
|
||||||
|
|
||||||
|
### 方法二:通过命令行
|
||||||
|
|
||||||
|
如果您的 VLE 是全局安装的,会自动存在一个命令行工具,此处以 vscode 为例子(trae 的命令为 trae),打开命令行,输入
|
||||||
|
|
||||||
|
```bash
|
||||||
|
code --install-extension /path/to/openmcp-0.1.1.vsix
|
||||||
|
```
|
||||||
|
|
||||||
|
/path/to/openmcp-0.1.1.vsix 代表你刚刚下载的 vsix 文件的绝对路径。这样也可以安装插件。
|
287
plugin-tutorial/concept.md
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
# MCP 基础概念
|
||||||
|
|
||||||
|
## 前言
|
||||||
|
|
||||||
|
在[之前的文章](https://zhuanlan.zhihu.com/p/28859732955)中,我们简单介绍了 MCP 的定义和它的基本组织结构。作为开发者,我们最需要关注的其实是如何根据我们自己的业务和场景定制化地开发我们需要的 MCP 服务器,这样直接接入任何一个 MCP 客户端后,我们都可以给大模型以我们定制出的交互能力。
|
||||||
|
|
||||||
|
在正式开始教大家如何开发自己的 MCP 服务器之前,我想,或许有必要讲清楚几个基本概念。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP Server 中的基本概念
|
||||||
|
|
||||||
|
### Resources, Prompts 和 Tools
|
||||||
|
|
||||||
|
在 [MCP 客户端协议](https://modelcontextprotocol.io/clients) 中,讲到了 MCP 协议中三个非常重要的能力类别:
|
||||||
|
|
||||||
|
- Resouces :定制化地请求和访问本地的资源,可以是文件系统、数据库、当前代码编辑器中的文件等等原本网页端的app 无法访问到的 **静态资源**。额外的 resources 会丰富发送给大模型的上下文,使得 AI 给我们更加精准的回答。
|
||||||
|
- Prompts :定制化一些场景下可供 AI 进行采纳的 prompt,比如如果需要 AI 定制化地返回某些格式化内容时,可以提供自定义的 prompts。
|
||||||
|
- Tools :可供 AI 使用的工具,它必须是一个函数,比如预定酒店、打开网页、关闭台灯这些封装好的函数就可以是一个 tool,大模型会通过 function calling 的方式来使用这些 tools。 Tools 将会允许 AI 直接操作我们的电脑,甚至和现实世界发生交互。
|
||||||
|
|
||||||
|
各位拥有前后端开发经验的朋友们,可以将 Resouces 看成是「额外给予大模型的只读权限」,把 Tools 看成是「额外给予大模型的读写权限」。
|
||||||
|
|
||||||
|
MCP 客户端(比如 Claude Desktop,5ire 等)已经实现好了上述的前端部分逻辑。而具体提供什么资源,具体提供什么工具,则需要各位玩家充分想象了,也就是我们需要开发丰富多彩的 MCP Server 来允许大模型做出更多有意思的工作。
|
||||||
|
|
||||||
|
不过需要说明的一点是,目前几乎所有大模型采用了 openai 协议作为我们访问大模型的接入点。什么叫 openai 协议呢?
|
||||||
|
|
||||||
|
### openai 协议
|
||||||
|
|
||||||
|
当我们使用 python 或者 typescript 开发 app 时,往往会安装一个名为 openai 的库,里面填入你需要使用的模型厂商、模型的基础 url、使用的模型类别来直接访问大模型。而各个大模型提供商也必须支持这个库,这套协议。
|
||||||
|
|
||||||
|
比如我们在 python 中访问 deepseek 的服务就可以这么做:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
client = OpenAI(api_key="<DeepSeek API Key>", base_url="https://api.deepseek.com")
|
||||||
|
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="deepseek-chat",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "You are a helpful assistant"},
|
||||||
|
{"role": "user", "content": "Hello"},
|
||||||
|
],
|
||||||
|
stream=False
|
||||||
|
)
|
||||||
|
|
||||||
|
print(response.choices[0].message.content)
|
||||||
|
```
|
||||||
|
|
||||||
|
如果你点进这个 create 函数去看,你会发现 openai 协议需要大模型厂家支持的 feature 是非常非常多的:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@overload
|
||||||
|
def create(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
messages: Iterable[ChatCompletionMessageParam],
|
||||||
|
model: Union[str, ChatModel],
|
||||||
|
audio: Optional[ChatCompletionAudioParam] | NotGiven = NOT_GIVEN,
|
||||||
|
frequency_penalty: Optional[float] | NotGiven = NOT_GIVEN,
|
||||||
|
function_call: completion_create_params.FunctionCall | NotGiven = NOT_GIVEN,
|
||||||
|
functions: Iterable[completion_create_params.Function] | NotGiven = NOT_GIVEN,
|
||||||
|
logit_bias: Optional[Dict[str, int]] | NotGiven = NOT_GIVEN,
|
||||||
|
logprobs: Optional[bool] | NotGiven = NOT_GIVEN,
|
||||||
|
max_completion_tokens: Optional[int] | NotGiven = NOT_GIVEN,
|
||||||
|
max_tokens: Optional[int] | NotGiven = NOT_GIVEN,
|
||||||
|
metadata: Optional[Metadata] | NotGiven = NOT_GIVEN,
|
||||||
|
modalities: Optional[List[Literal["text", "audio"]]] | NotGiven = NOT_GIVEN,
|
||||||
|
n: Optional[int] | NotGiven = NOT_GIVEN,
|
||||||
|
parallel_tool_calls: bool | NotGiven = NOT_GIVEN,
|
||||||
|
prediction: Optional[ChatCompletionPredictionContentParam] | NotGiven = NOT_GIVEN,
|
||||||
|
presence_penalty: Optional[float] | NotGiven = NOT_GIVEN,
|
||||||
|
reasoning_effort: Optional[ReasoningEffort] | NotGiven = NOT_GIVEN,
|
||||||
|
response_format: completion_create_params.ResponseFormat | NotGiven = NOT_GIVEN,
|
||||||
|
seed: Optional[int] | NotGiven = NOT_GIVEN,
|
||||||
|
service_tier: Optional[Literal["auto", "default"]] | NotGiven = NOT_GIVEN,
|
||||||
|
stop: Union[Optional[str], List[str], None] | NotGiven = NOT_GIVEN,
|
||||||
|
store: Optional[bool] | NotGiven = NOT_GIVEN,
|
||||||
|
stream: Optional[Literal[False]] | NotGiven = NOT_GIVEN,
|
||||||
|
stream_options: Optional[ChatCompletionStreamOptionsParam] | NotGiven = NOT_GIVEN,
|
||||||
|
temperature: Optional[float] | NotGiven = NOT_GIVEN,
|
||||||
|
tool_choice: ChatCompletionToolChoiceOptionParam | NotGiven = NOT_GIVEN,
|
||||||
|
tools: Iterable[ChatCompletionToolParam] | NotGiven = NOT_GIVEN,
|
||||||
|
top_logprobs: Optional[int] | NotGiven = NOT_GIVEN,
|
||||||
|
top_p: Optional[float] | NotGiven = NOT_GIVEN,
|
||||||
|
user: str | NotGiven = NOT_GIVEN,
|
||||||
|
web_search_options: completion_create_params.WebSearchOptions | NotGiven = NOT_GIVEN,
|
||||||
|
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
|
||||||
|
# The extra values given here take precedence over values defined on the client or passed to this method.
|
||||||
|
extra_headers: Headers | None = None,
|
||||||
|
extra_query: Query | None = None,
|
||||||
|
extra_body: Body | None = None,
|
||||||
|
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
|
||||||
|
) -> ChatCompletion:
|
||||||
|
```
|
||||||
|
|
||||||
|
从上面的签名中,你应该可以看到几个很熟悉的参数,比如 `temperature`, `top_p`,很多的大模型使用软件中,有的会给你暴露这个参数进行调节。比如在 5ire 中,内容随机度就是 `temperature` 这个参数的图形化显示。
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-9f8544aa917e8c128fc194adeb7161cd_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
其实如你所见,一次普普通通调用涉及到的可调控参数是非常之多的。而在所有参数中,你可以注意到一个参数叫做 `tools`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@overload
|
||||||
|
def create(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
messages: Iterable[ChatCompletionMessageParam],
|
||||||
|
model: Union[str, ChatModel],
|
||||||
|
audio: Optional[ChatCompletionAudioParam] | NotGiven = NOT_GIVEN,
|
||||||
|
|
||||||
|
# 看这里
|
||||||
|
tools: Iterable[ChatCompletionToolParam] | NotGiven = NOT_GIVEN,
|
||||||
|
) -> ChatCompletion:
|
||||||
|
```
|
||||||
|
|
||||||
|
### tool_calls 字段
|
||||||
|
|
||||||
|
在上面的 openai 协议中,有一个名为 tools 的参数。 tools 就是要求大模型厂商必须支持 function calling 这个特性,也就是我们提供一部分工具的描述(和 MCP 协议完全兼容的),在 tools 不为空的情况下,chat 函数返回的值中会包含一个特殊的字段 `tool_calls`,我们可以运行下面的我写好的让大模型调用可以查询天气的代码:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
api_key="Deepseek API",
|
||||||
|
base_url="https://api.deepseek.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 定义 tools(函数/工具列表)
|
||||||
|
tools = [
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_current_weather",
|
||||||
|
"description": "获取给定地点的天气",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"location": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "城市,比如杭州,北京,上海",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["location"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="deepseek-chat",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "你是一个很有用的 AI"},
|
||||||
|
{"role": "user", "content": "今天杭州的天气是什么?"},
|
||||||
|
],
|
||||||
|
tools=tools, # 传入 tools 参数
|
||||||
|
tool_choice="auto", # 可选:控制是否强制调用某个工具
|
||||||
|
stream=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(response.choices[0].message)
|
||||||
|
```
|
||||||
|
|
||||||
|
运行上述代码,它的返回如下:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ChatCompletionMessage(
|
||||||
|
content='',
|
||||||
|
refusal=None,
|
||||||
|
role='assistant',
|
||||||
|
annotations=None,
|
||||||
|
audio=None,
|
||||||
|
function_call=None,
|
||||||
|
tool_calls=[
|
||||||
|
ChatCompletionMessageToolCall(
|
||||||
|
id='call_0_baeaba2b-739d-40c2-aa6c-1e61c6d7e855',
|
||||||
|
function=Function(
|
||||||
|
arguments='{"location":"杭州"}',
|
||||||
|
name='get_current_weather'
|
||||||
|
),
|
||||||
|
type='function',
|
||||||
|
index=0
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
可以看到上面的 `tool_calls` 给出了大模型想要如何去使用我们给出的工具。需要说明的一点是,收到上下文的限制,目前一个问题能够让大模型调取的工具上限一般不会超过 100 个,这个和大模型厂商的上下文大小有关系。奥,对了,友情提示,当你使用 MCP 客户端在使用大模型解决问题时,同一时间激活的 MCP Server 越多,消耗的 token 越多哦 :D
|
||||||
|
|
||||||
|
而目前 openai 的协议中,tools 是只支持函数类的调用。而函数类的调用往往是可以模拟出 Resources 的效果的。比如取资源,你可以描述为一个 tool。因此在正常情况下,如果大家要开发 MCP Server,最好只开发 Tools,另外两个 feature 还暂时没有得到广泛支持。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
接下里就要让我们大显身手了!MCP 官方提供了几个封装好的 mcp sdk 来让我们快速开发一个 MCP 服务器。我看了一下,目前使用最爽,最简单的是 python 的 sdk,所以我就用 python 来简单演示一下了。
|
||||||
|
|
||||||
|
当然,如果想要使用 typescript 开发也是没问题的,typescript 的优势就是打包和部署更加简单。可以看我自用的一个模板库:[mcp-server-template](https://github.com/LSTM-Kirigaya/mcp-server-template)
|
||||||
|
|
||||||
|
### 安装基本环境
|
||||||
|
|
||||||
|
首先让我们打开一个项目,安装基本的库:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install mcp "mcp[cli]" uv
|
||||||
|
```
|
||||||
|
|
||||||
|
创建 `server.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
|
mcp = FastMCP('锦恢的 MCP Server', version="11.45.14")
|
||||||
|
|
||||||
|
@mcp.tool('add', '对两个数字进行实数域的加法')
|
||||||
|
def add(a: int, b: int) -> int:
|
||||||
|
return a + b
|
||||||
|
|
||||||
|
@mcp.resource("greeting://{name}", 'greeting', '用于演示的一个资源协议')
|
||||||
|
def get_greeting(name: str) -> str:
|
||||||
|
# 访问处理 greeting://{name} 资源访问协议,然后返回
|
||||||
|
# 此处方便起见,直接返回一个 Hello,balabala 了
|
||||||
|
return f"Hello, {name}!"
|
||||||
|
|
||||||
|
@mcp.prompt('translate', '进行翻译的prompt')
|
||||||
|
def translate(message: str) -> str:
|
||||||
|
return f'请将下面的话语翻译成中文:\n\n{message}'
|
||||||
|
```
|
||||||
|
|
||||||
|
上面的代码在装饰器的作用下非常容易读懂,`@mcp.tool`, `@mcp.resource` 和 `@mcp.prompt` 分别实现了
|
||||||
|
|
||||||
|
- 一个名为 add 的 tool
|
||||||
|
- 一个 greeting 协议的 resource
|
||||||
|
- 一个名为 translate 的 prompt
|
||||||
|
|
||||||
|
> 不明白装饰器是什么小白可以看看我之前的文章[Python进阶笔记(一)装饰器实现函数/类的注册](https://zhuanlan.zhihu.com/p/350821621)
|
||||||
|
> 虽然注册器里面的第二个参数 description 不是必要的,但是仍然建议写一下,要不然大模型怎么知道你这个工具是干啥的。
|
||||||
|
|
||||||
|
### 使用 Inspector 进行调试
|
||||||
|
|
||||||
|
我们可以使用 MCP 官方提供的 Inspector 工具对上面的 server 进行调试:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcp dev server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
这会启动一个前端服务器并,打开 `http://localhost:5173/` 后我们可以看到 inspector 的调试界面,先点击左侧的 `Connect` 来运行我们的 server.py 并通过 stdio 为通信管道和 web 建立通信。
|
||||||
|
|
||||||
|
Fine,可以开始愉快地进行调试了,Inspector 主要给了我们三个板块,分别对应 Resources,Prompts 和 Tools。
|
||||||
|
|
||||||
|
先来看 Resources,点击「Resource Templates」可以罗列所有注册的 Resource,比如我们上文定义的 `get_greeting`,你可以通过输入参数运行来查看这个函数是否正常工作。(因为一般情况下的这个资源协议是会访问远程数据库或者微服务的)
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-71fc1ad813cdbf7ecec24d878c343b96_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Prompts 端就比较简单了,直接输入预定义参数就能获取正常的返回结果。
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://pic1.zhimg.com/80/v2-4f42899ba1163922ac2347f7cebe5362_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Tools 端将会是我们后面调试的核心。在之前的章节我们讲过了,MCP 协议中的 Prompts 和 Resources 目前还没有被 openai 协议和各大 MCP 客户端广泛支持,因此,我们主要的服务端业务都应该是在写 tools。
|
||||||
|
|
||||||
|
我们此处提供的 tool 是实现一个简单的加法,它非常简单,我们输入 1 和 2 就可以直接看到结果是 3。我们后续会开发一个可以访问天气预报的 tool,那么到时候就非常需要一个这样的窗口来调试我们的天气信息获取是否正常了。
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://pic1.zhimg.com/80/v2-4164a900198a70a158ae441f9e441d07_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 结余
|
||||||
|
|
||||||
|
这篇文章,我们简单了解了 MCP 内部的一些基本概念,我认为这些概念对于诸位开发一个 MCP 服务器是大有裨益的,所以我认为有必要先讲一讲。
|
||||||
|
|
||||||
|
下面的文章中,我将带领大家探索 MCP 的奇境,一个属于 AI Agent 的时代快要到来了。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 挖坑
|
||||||
|
|
||||||
|
从上面的例子中,大家也能看出其实现在调试 MCP Server 的工具还不算齐全,所以我打算最近快速开发一款 vscode 插件,集合 Inspector 的所有功能和基础的大模型测试为一体。如果你开发了基本的网络和磁盘访问的 MCP Server,这个调试工具也可以当成一个 Manus 客户端进行把玩。
|
||||||
|
|
||||||
|
请大家期待吧!
|
473
plugin-tutorial/examples/go-neo4j-sse.md
Normal file
@ -0,0 +1,473 @@
|
|||||||
|
# go 实现 neo4j 的只读 mcp 服务器 (SSE)
|
||||||
|
|
||||||
|
## 前言
|
||||||
|
|
||||||
|
本篇教程,演示一下如何使用 go 语言写一个可以访问 neo4j 数据库的 mcp 服务器。实现完成后,我们不需要写任何 查询代码 就能通过询问大模型了解服务器近况。
|
||||||
|
|
||||||
|
不同于之前的连接方式,这次,我们将采用 SSE 的方式来完成服务器的创建和连接。
|
||||||
|
|
||||||
|
本期教程的代码:https://github.com/LSTM-Kirigaya/openmcp-tutorial/tree/main/neo4j-go-server
|
||||||
|
|
||||||
|
建议下载本期的代码,因为里面有我为大家准备好的数据库文件。要不然,你们得自己 mock 数据了。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 准备
|
||||||
|
|
||||||
|
项目结构如下:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
📦neo4j-go-server
|
||||||
|
┣ 📂util
|
||||||
|
┃ ┗ 📜util.go # 工具函数
|
||||||
|
┣ 📜main.go # 主函数
|
||||||
|
┗ 📜neo4j.json # 数据库连接的账号密码
|
||||||
|
```
|
||||||
|
|
||||||
|
我们先创建一个 go 项目:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir neo4j-go-server
|
||||||
|
cd neo4j-go-server
|
||||||
|
go mod init neo4j-go-server
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 完成数据库初始化
|
||||||
|
|
||||||
|
### 2.1 安装 neo4j
|
||||||
|
|
||||||
|
首先,根据我的教程在本地或者服务器配置一个 neo4j 数据库,这里是是教程,你只需要完成该教程的前两步即可: [neo4j 数据库安装与配置](https://kirigaya.cn/blog/article?seq=199)。将 bin 路径加入环境变量,并且设置的密码设置为 openmcp。
|
||||||
|
|
||||||
|
然后在 main.go 同级下创建 neo4j.json,填写 neo4j 数据库的连接信息:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"url" : "neo4j://localhost:7687",
|
||||||
|
"name" : "neo4j",
|
||||||
|
"password" : "openmcp"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 导入事先准备好的数据
|
||||||
|
|
||||||
|
安装完成后,大家可以导入我实现准备好的数据,这些数据是我的个人网站上部分数据脱敏后的摘要,大家可以随便使用,下载链接:[neo4j.db](https://github.com/LSTM-Kirigaya/openmcp-tutorial/releases/download/neo4j.db/neo4j.db)。下载完成后,运行下面的命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
neo4j stop
|
||||||
|
neo4j-admin load --database neo4j --from neo4j.db --force
|
||||||
|
neo4j start
|
||||||
|
```
|
||||||
|
|
||||||
|
然后,我们登录数据库就能看到我准备好的数据啦:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cypher-shell -a localhost -u neo4j -p openmcp
|
||||||
|
```
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://pic1.zhimg.com/80/v2-4b53ad6a355c05d99c7ed18687ced717_1440w.png" style="width: 80%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### 2.3 验证 go -> 数据库连通性
|
||||||
|
|
||||||
|
为了验证数据库的连通性和 go 的数据库驱动是否正常工作,我们需要先写一段数据库访问的最小系统。
|
||||||
|
|
||||||
|
先安装 neo4j 的 v5 版本的 go 驱动:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/neo4j/neo4j-go-driver/v5
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `util.go` 中添加以下代码:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Neo4jDriver neo4j.DriverWithContext
|
||||||
|
)
|
||||||
|
|
||||||
|
// 创建 neo4j 服务器的连接
|
||||||
|
func CreateNeo4jDriver(configPath string) (neo4j.DriverWithContext, error) {
|
||||||
|
jsonString, _ := os.ReadFile(configPath)
|
||||||
|
config := make(map[string]string)
|
||||||
|
|
||||||
|
json.Unmarshal(jsonString, &config)
|
||||||
|
// fmt.Printf("url: %s\nname: %s\npassword: %s\n", config["url"], config["name"], config["password"])
|
||||||
|
|
||||||
|
var err error
|
||||||
|
Neo4jDriver, err = neo4j.NewDriverWithContext(
|
||||||
|
config["url"],
|
||||||
|
neo4j.BasicAuth(config["name"], config["password"], ""),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return Neo4jDriver, err
|
||||||
|
}
|
||||||
|
return Neo4jDriver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 执行只读的 cypher 查询
|
||||||
|
func ExecuteReadOnlyCypherQuery(
|
||||||
|
cypher string,
|
||||||
|
) ([]map[string]any, error) {
|
||||||
|
session := Neo4jDriver.NewSession(context.TODO(), neo4j.SessionConfig{
|
||||||
|
AccessMode: neo4j.AccessModeRead,
|
||||||
|
})
|
||||||
|
|
||||||
|
defer session.Close(context.TODO())
|
||||||
|
|
||||||
|
result, err := session.Run(context.TODO(), cypher, nil)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var records []map[string]any
|
||||||
|
for result.Next(context.TODO()) {
|
||||||
|
records = append(records, result.Record().AsMap())
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
main.go 中添加以下代码:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
|
||||||
|
"neo4j-go-server/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
neo4jPath string = "./neo4j.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
_, err := util.CreateNeo4jDriver(neo4jPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Neo4j driver created successfully")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
运行主程序来验证数据库的连通性:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
如果输出了 `Neo4j driver created successfully`,则说明数据库的连通性验证通过。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 实现 mcp 服务器
|
||||||
|
|
||||||
|
go 的 mcp 的 sdk 最为有名的是 mark3labs/mcp-go 了,我们就用这个。
|
||||||
|
|
||||||
|
> mark3labs/mcp-go 的 demo 在 https://github.com/mark3labs/mcp-go,非常简单,此处直接使用即可。
|
||||||
|
|
||||||
|
先安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/mark3labs/mcp-go
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在 `main.go` 中添加以下代码:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ... existing code ...
|
||||||
|
|
||||||
|
var (
|
||||||
|
addr string = "localhost:8083"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// ... existing code ...
|
||||||
|
|
||||||
|
s := server.NewMCPServer(
|
||||||
|
"只读 Neo4j 服务器",
|
||||||
|
"0.0.1",
|
||||||
|
server.WithToolCapabilities(true),
|
||||||
|
)
|
||||||
|
|
||||||
|
srv := server.NewSSEServer(s)
|
||||||
|
|
||||||
|
// 定义 executeReadOnlyCypherQuery 这个工具的 schema
|
||||||
|
executeReadOnlyCypherQuery := mcp.NewTool("executeReadOnlyCypherQuery",
|
||||||
|
mcp.WithDescription("执行只读的 Cypher 查询"),
|
||||||
|
mcp.WithString("cypher",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("Cypher 查询语句,必须是只读的"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 将真实函数和申明的 schema 绑定
|
||||||
|
s.AddTool(executeReadOnlyCypherQuery, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
cypher := request.Params.Arguments["cypher"].(string)
|
||||||
|
result, err := util.ExecuteReadOnlyCypherQuery(cypher)
|
||||||
|
|
||||||
|
fmt.Println(result)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultText(""), err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(fmt.Sprintf("%v", result)), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// 在 http://localhost:8083/sse 开启服务
|
||||||
|
fmt.Printf("Server started at http://%s/sse\n", addr)
|
||||||
|
srv.Start(addr)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
go run main.go 运行上面的代码,你就能看到如下信息:
|
||||||
|
|
||||||
|
```
|
||||||
|
Neo4j driver created successfully
|
||||||
|
Server started at http://localhost:8083/sse
|
||||||
|
```
|
||||||
|
|
||||||
|
说明我们的 mcp 服务器在本地的 8083 上启动了。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 通过 openmcp 来进行调试
|
||||||
|
|
||||||
|
### 4.1 添加工作区 sse 调试项目
|
||||||
|
|
||||||
|
接下来,我们来通过 openmcp 进行调试,先点击 vscode 左侧的 openmcp 图标进入控制面板,如果你是下载的 https://github.com/LSTM-Kirigaya/openmcp-tutorial/tree/main/neo4j-go-server 这个项目,那么你能看到【MCP 连接(工作区)】里面已经有一个创建好的调试项目【只读 Neo4j 服务器】了。如果你是完全自己做的这个项目,可以通过下面的按钮添加连接,选择 sse 后填入 http://localhost:8083/sse,oauth 空着不填即可。
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-31a01f1253dfc8c42e23e05b1869a932_1440w.png" style="width: 80%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### 4.2 测试工具
|
||||||
|
|
||||||
|
第一次调试 mcp 服务器要做的事情一定是先调通 mcp tool,新建标签页,选择 tool,点击下图的工具,输入 `CALL db.labels() YIELD label RETURN label`,这个语句是用来列出所有节点类型的。如果输出下面的结果,说明当前的链路生效,没有问题。
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://pic1.zhimg.com/80/v2-dd59d9c96ecb455e527ab8aa7f963908_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
### 4.3 摸清大模型功能边界,用提示词来封装我们的知识
|
||||||
|
|
||||||
|
然后,让我们做点有趣的事情吧!我们接下来要测试一下大模型的能力边界,因为 neo4j 属于特种数据库,通用大模型不一定知道怎么用它。新建标签页,点击「交互测试」,我们先问一个简单的问题:
|
||||||
|
|
||||||
|
```
|
||||||
|
帮我找出最新的 10 条评论
|
||||||
|
```
|
||||||
|
|
||||||
|
结果如下:
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-44fab30650051db4e3b94de34275af3a_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
可以看到,大模型查询的节点类型就是错误的,在我提供的例子中,代表评论的节点是 BlogComment,而不是 Comment。也就是说,大模型并不掌握进行数据库查询的通用方法论。这就是我们目前知道的它的能力边界。我们接下来要一步一步地注入我们的经验和知识,唔姆,通过 system prompt 来完成。
|
||||||
|
|
||||||
|
### 4.4 教大模型找数据库节点
|
||||||
|
|
||||||
|
好好想一下,作为工程师的我们是怎么知道评论的节点是 BlogComment?我们一般是通过罗列当前数据库的所有节点的类型来从命名中猜测的,比如,对于这个数据库,我一般会先输入如下的 cypher 查询:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CALL db.labels() YIELD label RETURN label
|
||||||
|
```
|
||||||
|
|
||||||
|
它的输出就在 4.2 的图中,如果你的英文不错,也能看出来 BlogComment 大概率是代表博客评论的节点。好了,那么我们将这段方法论注入到 system prompt 中,从而封装我们的这层知识,点击下图的下方的按钮,进入到【系统提示词】:
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://pica.zhimg.com/80/v2-e0fdd265e53dd354163358be1f5cc3f6_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
新建提示词【neo4j】,输入:
|
||||||
|
|
||||||
|
```
|
||||||
|
你是一个善于进行neo4j查询的智能体,对于用户要求的查询请求,你并不一定知道对应的数据库节点是什么,这个时候,你需要先列出所有的节点类型,然后从中找到你认为最有可能是匹配用户询问的节点。比如用户问你要看符合特定条件的「文章」,你并不知道文章的节点类型是什么,这个时候,你就需要先列出所有的节点。
|
||||||
|
```
|
||||||
|
|
||||||
|
点击保存,然后在【交互测试】中,重复刚才的问题:
|
||||||
|
|
||||||
|
```
|
||||||
|
帮我找出最新的 10 条评论
|
||||||
|
```
|
||||||
|
|
||||||
|
大模型的回答如下:
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-ccf4a5ecb5691620fca659dcd60d2e38_1440w.png" style="width: 80%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
诶?怎么说,是不是好了很多了?大模型成功找到了 BlogComment 这个节点,然后返回了对应的数据。
|
||||||
|
|
||||||
|
但是其实还是不太对,因为我们要求的说最新的 10 条评论,但是大模型返回的其实是最早的 10 条评论,我们点开大模型的调用细节就能看到,大模型是通过 `ORDER BY comment.createdAt` 来实现的,但是问题是,在我们的数据库中,记录一条评论何时创建的字段并不是 createdAt,而是 createdTime,这意味着大模型并不知道自己不知道节点的字段,从而产生了「幻觉」,瞎输入了一个字段。
|
||||||
|
|
||||||
|
大模型是不会显式说自己不知道的,锦恢研究生关于 OOD 的一项研究可以说明这件事的本质原因:[EDL(Evidential Deep Learning) 原理与代码实现](https://kirigaya.cn/blog/article?seq=154),如果阁下的好奇心能够配得上您的数学功底,可以一试这篇文章。总之,阁下只需要知道,正因为大模型对自己不知道的东西会产生幻觉,所以才有我们得以注入经验的操作空间。
|
||||||
|
|
||||||
|
### 4.5 教大模型找数据库节点的字段
|
||||||
|
|
||||||
|
通过上面的尝试,我们知道我们距离终点只剩一点了,那就是告诉大模型,我们的数据库中,记录一条评论何时创建的字段并不是 createdAt,而是 createdTime。
|
||||||
|
|
||||||
|
对于识别字段的知识,我们改良一下刚刚的系统提示词下:
|
||||||
|
|
||||||
|
```
|
||||||
|
你是一个善于进行neo4j查询的智能体,对于用户要求的查询请求,你并不一定知道对应的数据库节点是什么,这个时候,你需要先列出所有的节点类型,然后从中找到你认为最有可能是匹配用户询问的节点。比如用户问你要看符合特定条件的「文章」,你并不知道文章的节点类型是什么,这个时候,你就需要先列出所有的节点。
|
||||||
|
|
||||||
|
对于比较具体的查询,你需要先查询单个事例来看一下当前类型有哪些字段。比如用户问你最新的文章,你是不知道文章节点的哪一个字段代表 「创建时间」的,因此,你需要先列出一到两个文章节点,看一下里面有什么字段,然后再创建查询查看最新的10篇文章。
|
||||||
|
```
|
||||||
|
|
||||||
|
结果如下:
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-e7a2faf43249fe108288604a2eb948ad_1440w.png" style="width: 80%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
是不是很完美?
|
||||||
|
|
||||||
|
通过使用 openmcp 调试,我们可以通过 system prompt + mcp server 来唯一确定一个 agent 的表现行为。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 扩充 mcp 服务器的原子技能
|
||||||
|
|
||||||
|
在上面的例子中,虽然我们通过 system prompt 注入了我们的经验和知识,但是其实你会发现这些我们注入的行为,比如「查询所有节点类型」和「获取一个节点的所有字段」,是不是流程很固定?但是 system prompt 是通过自然语言编写的,它具有语言特有的模糊性,我们无法保证它一定是可以拓展的。那么除了 system prompt,还有什么方法可以注入我们的经验与知识呢?有的,兄弟,有的。
|
||||||
|
|
||||||
|
在这种流程固定,而且这个操作也非常地容易让「稍微有点经验的人」也能想到的情况下,除了使用 system prompt 外,我们还有一个方法可以做到更加标准化地注入知识,也就是把上面的这些个流程写成额外的 mcp tool。这个方法被我称为「原子化扩充」(Atomization Supplement)。
|
||||||
|
|
||||||
|
所谓原子化扩充,也就是增加额外的 mcp tool,这些 tool 在功能层面是「原子化」的。
|
||||||
|
|
||||||
|
> 满足如下条件之一的 tool,被称为 原子 tool (Atomic Tool):
|
||||||
|
> tool 无法由更加细粒度的功能通过有限组合得到
|
||||||
|
> 组成得到 tool 的更加细粒度的功能,大模型并不会完全使用,或者使用不可靠 (比如汇编语言,比如 DOM 查询)
|
||||||
|
|
||||||
|
扩充额外的原子 tool,能够让大模型知道 “啊!我还有别的手段可以耍!” ,那么只要 description 比较恰当,大模型就能够使用它们来获得额外的信息,而不是产生「幻觉」让任务失败。
|
||||||
|
|
||||||
|
对于上面的一整套流程,我们目前知道了如下两个技能大模型是会产生「幻觉」的:
|
||||||
|
|
||||||
|
1. 获取一个节点类别的标签(询问评论,大模型没说自己不知道什么是评论标签,而是直接使用了 Comment,但是实际的评论标签是 BlogComment)
|
||||||
|
2. 获取一个节点类别的字段(询问最新评论,大模型选择通过 createAt 排序,但是记录 BlogComment 创建时间的字段是 createTime)
|
||||||
|
|
||||||
|
在之前,我们通过了 system prompt 来完成了信息的注入,现在,丢弃你的 system prompt 吧!我们来玩点更加有趣的游戏。在刚刚的 util.go 中,我们针对上面的两个幻觉,实现两个额外的函数 (经过测试,cursor或者trae能完美生成下面的代码,可以不用自己写):
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 获取所有的节点类型
|
||||||
|
func GetAllNodeTypes() ([]string, error) {
|
||||||
|
cypher := "MATCH (n) RETURN DISTINCT labels(n) AS labels"
|
||||||
|
result, err := ExecuteReadOnlyCypherQuery(cypher)
|
||||||
|
if err!= nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var nodeTypes []string
|
||||||
|
for _, record := range result {
|
||||||
|
labels := record["labels"].([]any)
|
||||||
|
for _, label := range labels {
|
||||||
|
nodeTypes = append(nodeTypes, label.(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodeTypes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取一个节点的字段示范
|
||||||
|
func GetNodeFields(nodeType string) ([]string, error) {
|
||||||
|
cypher := fmt.Sprintf("MATCH (n:%s) RETURN keys(n) AS keys LIMIT 1", nodeType)
|
||||||
|
result, err := ExecuteReadOnlyCypherQuery(cypher)
|
||||||
|
if err!= nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var fields []string
|
||||||
|
for _, record := range result {
|
||||||
|
keys := record["keys"].([]any)
|
||||||
|
for _, key := range keys {
|
||||||
|
fields = append(fields, key.(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fields, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在 main.go 中完成它们的 schema 的申明和 tool 的注册:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ... existing code ...
|
||||||
|
|
||||||
|
getAllNodeTypes := mcp.NewTool("getAllNodeTypes",
|
||||||
|
mcp.WithDescription("获取所有的节点类型"),
|
||||||
|
)
|
||||||
|
|
||||||
|
getNodeField := mcp.NewTool("getNodeField",
|
||||||
|
mcp.WithDescription("获取节点的字段"),
|
||||||
|
mcp.WithString("nodeLabel",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("节点的标签"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 注册对应的工具到 schema 上
|
||||||
|
s.AddTool(getAllNodeTypes, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
result, err := util.GetAllNodeTypes()
|
||||||
|
|
||||||
|
fmt.Println(result)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultText(""), err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(fmt.Sprintf("%v", result)), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddTool(getNodeField, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
nodeLabel := request.Params.Arguments["nodeLabel"].(string)
|
||||||
|
result, err := util.GetNodeFields(nodeLabel)
|
||||||
|
|
||||||
|
fmt.Println(result)
|
||||||
|
|
||||||
|
if err!= nil {
|
||||||
|
return mcp.NewToolResultText(""), err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(fmt.Sprintf("%v", result)), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// ... existing code ...
|
||||||
|
```
|
||||||
|
|
||||||
|
重新运行 sse 服务器,然后直接询问大模型,此时,我们取消使用 system prompt(创建一个空的,或者直接把当前的 prompt 删除),询问结果如下:
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-1e88f7d8e04b949040a02673c13d6462_1440w.png" style="width: 80%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
可以看到,在没有 system prompt 的情况下,大模型成功执行了这个过程,非常完美。
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
这期教程,带大家使用 go 走完了 mcp sse 的连接方式,并且做出了一个「只读 neo4j 数据库」的 mcp,通过这个 mcp,我们可以非常方便地用自然语言查询数据库的结果,而不需要手动输入 cypher。
|
||||||
|
|
||||||
|
对于部分情况下,大模型因为「幻觉」问题而导致的任务失败,我们通过一步步有逻辑可遵循的方法论,完成了 system prompt 的调优和知识的封装。最终,通过范式化的原子化扩充的方式,将这些知识包装成了更加完善的 mcp 服务器。这样,任何人都可以直接使用你的 mcp 服务器来完成 neo4j 数据库的自然语言查询了。
|
||||||
|
|
||||||
|
最后,觉得 openmcp 好用的米娜桑,别忘了给我们的项目点个 star:https://github.com/LSTM-Kirigaya/openmcp-client
|
||||||
|
|
||||||
|
想要和我进一步交流 OpenMCP 的朋友,可以进入我们的交流群(github 项目里面有)
|
0
plugin-tutorial/examples/java-es-http.md
Normal file
436
plugin-tutorial/examples/python-simple-stdio.md
Normal file
@ -0,0 +1,436 @@
|
|||||||
|
# python 实现天气信息 mcp 服务器
|
||||||
|
|
||||||
|
## hook
|
||||||
|
|
||||||
|
等等,开始前,先让我们看一个小例子,假设我下周要去明日方舟锈影新生的漫展,所以我想要知道周六杭州的天气,于是我问大模型周六的天气,结果大模型给了我如下的回复:
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-4c623ac6897e12093535b0d9ed9cf242_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
这可不行,相信朋友们也经常遇到过这样的情况,大模型总是会“授人以渔”,但是有的时候,我们往往就是想要直接知道最终结果,特别是一些无聊的生活琐事。
|
||||||
|
|
||||||
|
其实实现天气预报的程序也很多啦,那么有什么方法可以把写好的天气预报的程序接入大模型,让大模型告诉我们真实的天气情况,从而选择明天漫展的穿搭选择呢?
|
||||||
|
|
||||||
|
如果直接写函数用 function calling 显得有点麻烦,这里面涉及到很多麻烦的技术细节需要我们商榷,比如大模型提供商的API调用呀,任务循环的搭建呀,文本渲染等等,从而浪费我们宝贵的时间。而 MCP 给了我们救赎之道,今天这期教程,就教大家写一个简单的 MCP 服务器,可以让大模型拥有得知天气预报的能力。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前言
|
||||||
|
|
||||||
|
👉 [上篇导航](https://zhuanlan.zhihu.com/p/32593727614)
|
||||||
|
|
||||||
|
在上篇,我们简单讲解了 MCP 的基础,在这一篇,我们将正式开始着手开发我们自己的 MCP 服务器,从而将现成的应用,服务,硬件等等接入大模型。从而走完大模型到赋能终端应用的最后一公里。
|
||||||
|
|
||||||
|
工欲善其事,必先利其器。为了更加优雅快乐地开发 MCP 服务器,我们需要一个比较好的测试工具,允许我们在开发的过程看到程序的变化,并且可以直接接入大模型验证工具的有效性。
|
||||||
|
|
||||||
|
于是,我在前不久开源了一款一体化的 MCP 测试开发工具 —— OpenMCP,[全网第一个 MCP 服务器一体化开发测试软件 OpenMCP 发布!](https://zhuanlan.zhihu.com/p/1894785817186121106)
|
||||||
|
|
||||||
|
> OpenMCP QQ 群 782833642
|
||||||
|
|
||||||
|
OpenMCP 开源链接:https://github.com/LSTM-Kirigaya/openmcp-client
|
||||||
|
|
||||||
|
求个 star :D
|
||||||
|
|
||||||
|
### 第一个 MCP 项目
|
||||||
|
|
||||||
|
事已至此,先 coding 吧 :D
|
||||||
|
|
||||||
|
在打开 vscode 或者 trae 之前,先安装基本的 uv 工具,uv 是一款社区流行的版本管理工具,你只需要把它理解为性能更好的 conda 就行了。
|
||||||
|
|
||||||
|
我们先安装 uv,如果您正在使用 anaconda,一定要切换到 base 环境,再安装:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install uv
|
||||||
|
```
|
||||||
|
|
||||||
|
安装完成后,运行 uv
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv
|
||||||
|
```
|
||||||
|
|
||||||
|
没有报错就说明成功。uv 只会将不可以复用的依赖安装在本地,所以使用 anaconda 的朋友不用担心,uv 安装的依赖库会污染你的 base,我们接下来使用 uv 来创建一个基础的 python 项目
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir simple-mcp && cd simple-mcp
|
||||||
|
uv init
|
||||||
|
uv add mcp "mcp[cli]"
|
||||||
|
```
|
||||||
|
|
||||||
|
然后我们打开 vscode 或者 trae,在插件商城找到并下载 OpenMCP 插件
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-525c4576398078547fdd6eeef26532aa_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
先制作一个 MCP 的最小程序:
|
||||||
|
|
||||||
|
文件名:simple_mcp.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
|
mcp = FastMCP('锦恢的 MCP Server', version="11.45.14")
|
||||||
|
|
||||||
|
@mcp.tool(
|
||||||
|
name='add',
|
||||||
|
description='对两个数字进行实数域的加法'
|
||||||
|
)
|
||||||
|
def add(a: int, b: int) -> int:
|
||||||
|
return a + b
|
||||||
|
|
||||||
|
@mcp.resource(
|
||||||
|
uri="greeting://{name}",
|
||||||
|
name='greeting',
|
||||||
|
description='用于演示的一个资源协议'
|
||||||
|
)
|
||||||
|
def get_greeting(name: str) -> str:
|
||||||
|
# 访问处理 greeting://{name} 资源访问协议,然后返回
|
||||||
|
# 此处方便起见,直接返回一个 Hello,balabala 了
|
||||||
|
return f"Hello, {name}!"
|
||||||
|
|
||||||
|
@mcp.prompt(
|
||||||
|
name='translate',
|
||||||
|
description='进行翻译的prompt'
|
||||||
|
)
|
||||||
|
def translate(message: str) -> str:
|
||||||
|
return f'请将下面的话语翻译成中文:\n\n{message}'
|
||||||
|
|
||||||
|
@mcp.tool(
|
||||||
|
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}"
|
||||||
|
```
|
||||||
|
|
||||||
|
我们试着运行它:
|
||||||
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run mcp run simple_mcp.py
|
||||||
|
```
|
||||||
|
|
||||||
|
如果没有报错,但是卡住了,那么说明我们的依赖安装没有问题,按下 ctrl c 或者 ctrl z 退出即可。
|
||||||
|
|
||||||
|
在阁下看起来,这些函数都简单到毫无意义,但是请相信我,我们总需要一些简单的例子来通往最终的系统。
|
||||||
|
|
||||||
|
### Link, Start!
|
||||||
|
|
||||||
|
如果你下载了 OpenMCP 插件,那么此时你就能在打开的 python 编辑器的右上角看到 OpenMCP 的紫色图标,点击它就能启动 OpenMCP,调试当前的 MCP 了。
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-f67e000371095a755d2f0d613706d61c_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
默认是以 STDIO 的方式启动,默认运行如下的命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run mcp run <当前打开的 python 文件的相对路径>
|
||||||
|
```
|
||||||
|
|
||||||
|
所以你需要保证已经安装了 mcp 脚手架,也就是 `uv add mcp "mcp[cli]"`。
|
||||||
|
|
||||||
|
打开后第一件事就是先看左下角连接状态,确保是绿色的,代表当前 OpenMCP 和你的 MCP 服务器已经握手成功。
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-c4ebbbfe98d51e8b6e7de6c6d1bceb2e_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
如果连接成功,此时连接上方还会显示你当前的 MCP 服务器的名字,光标移动上去还能看到版本号。这些信息由我们如下的代码定义:
|
||||||
|
|
||||||
|
```python
|
||||||
|
mcp = FastMCP('锦恢的 MCP Server', version="11.45.14")
|
||||||
|
```
|
||||||
|
|
||||||
|
这在我们进行版本管理的时候会非常有用。请善用这套系统。
|
||||||
|
|
||||||
|
|
||||||
|
如果连接失败,可以点击左侧工具栏的第二个按钮,进入连接控制台,查看错误信息,或是手动调整连接命令:
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://pic1.zhimg.com/80/v2-684190b98dbbb9a7bf0e8c8048bd1277_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### 初识 OpenMCP
|
||||||
|
|
||||||
|
接下来,我来简单介绍一下 OpenMCP 的基本功能模块,如果一开始,你的屏幕里什么也没有,先点击上面的加号创建一个新的标签页,此处页面中会出现下图屏幕中的四个按钮
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-3a4e8aa1ddaac632601532bb757a15ad_1440w.png?source=d16d100b" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
放大一点
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-ecc0705ed534e2cf0bc748ecd95f5f22_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
前三个,资源、提词和工具,分别用于调试 MCP 中的三个对应项目,也就是 Resources,Prompts 和 Tools,这三个部分的使用,基本和 MCP 官方的 Inspector 工具是一样的,那是自然,我就照着这几个抄的,诶嘿。
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://pica.zhimg.com/80/v2-d767e782f667161442ea183f55ca49b1_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
然后第四个按钮「交互测试」,它是一个我开发的 MCP 客户端,其实就是一个对话窗口,你可以无缝衔接地直接在大模型中测试你当前的 MCP 服务器的功能函数。
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-b59ee2d290e096343fb4659baf34cf57_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
目前我暂时只支持 tools 的支持,因为 prompts 和 resources 的我还没有想好,(resource 感觉就是可以当成一个 tool),欢迎大家进群和我一起讨论:QQ群 782833642
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 开始调试天气函数
|
||||||
|
|
||||||
|
### 工具调试
|
||||||
|
|
||||||
|
还记得我们一开始给的 mcp 的例子吗?我们可以通过 OpenMCP 来快速调试这里面写的函数,比如我们本期的目标,写一个天气预报的 MCP,那么假设我们已经写好了一个天气预报的函数了,我们把它封装成一个 tool:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@mcp.tool(
|
||||||
|
name='weather',
|
||||||
|
description='获取指定城市的天气信息'
|
||||||
|
)
|
||||||
|
def get_weather(city: str) -> str:
|
||||||
|
"""模拟天气查询协议,返回格式化字符串"""
|
||||||
|
return f"Weather in {city}: Sunny, 25°C"
|
||||||
|
```
|
||||||
|
|
||||||
|
当然,它现在是没有意义的,因为就算把黑龙江的城市ID输入,它也返回 25 度,但是这些都不重要,我想要带阁下先走完整套流程。建立自上而下的感性认知比死抠细节更加容易让用户学懂。
|
||||||
|
|
||||||
|
那么我们现在需要调试这个函数,打开 OpenMCP,新建一个「工具」调试项目
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-1c67ab54d67023e408413484768377cf_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
然后此时,你在左侧的列表可以看到 weather 这个工具,选择它,然后在右侧的输入框中随便输入一些东西,按下回车(或者点击「运行」),你能看到如下的响应:
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-d32a9c0d9fcab497dc03152a72c4c62b_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
看到我们函数 return 的字符串传过来了,说明没问题,链路通了。
|
||||||
|
|
||||||
|
### 交互测试
|
||||||
|
|
||||||
|
诶?我知道你编程很厉害,但是,在噼里啪啦快速写完天气预报爬虫前,我们现在看看我们要如何把已经写好的工具注入大模型对话中。为了使用大模型,我们需要先选择大模型和对应的 API,点击左侧工具栏的第三个按钮,进入 API 模块,选择你想要使用的大模型运营商、模型,填写 API token,然后点击下面的「保存」
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://pic1.zhimg.com/80/v2-367780b204d2aa50354585272b71af20_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
再新建一个标签页,选择「交互测试」,此时,我们就可以直接和大模型对话了,我们先看看没有任何工具注入的大模型会如何回应天气预报的问题,点击最下侧工具栏从左往右第三个按钮,进入工具选择界面,选择「禁用所有工具」
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://pic1.zhimg.com/80/v2-977a53ea14eae5e1a646fc73d379a422_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
点击「关闭」后,我们问大模型一个问题:
|
||||||
|
|
||||||
|
```
|
||||||
|
请问杭州的温度是多少?
|
||||||
|
```
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://pic1.zhimg.com/80/v2-d3aa56602f574a6968295f9a5c93438f_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
可以看到,大模型给出了和文章开头一样的回答。非常敷衍,因为它确实无法知道。
|
||||||
|
|
||||||
|
此处,我们再单独打开「weather」工具:
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-2ed66eaff604d11d52f60201fca215d4_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
问出相同的问题:
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-e934d386e20b1de43fb5e0dd426de86e_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
可以看到,大模型给出了回答是 25 度,还有一些额外的推导信息。
|
||||||
|
|
||||||
|
我们不妨关注一些细节,首先,大模型并不会直接回答问题,而是会先去调用 weather 这个工具,调用参数为:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"city": "杭州"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后,我们的 MCP 服务器给出了响应:
|
||||||
|
|
||||||
|
```
|
||||||
|
Weather in 杭州: Sunny, 25°C
|
||||||
|
```
|
||||||
|
|
||||||
|
从而,最终大模型才根据这些信息给出了最终的回答。也就是,这个过程我们实际调用了两次大模型的服务。而且可以看到两次调用的输入 token 数量都非常大,这是因为 OpenMCP 会将函数调用以 JSON Schema 的形式注入到请求参数中,weather 这个工具的 JSON Schema 如下图的右侧的 json 所示:
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-2ed66eaff604d11d52f60201fca215d4_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
然后支持 openai 协议的大模型厂商都会针对这样的信息进行 function calling,所以使用了工具的大模型请求的输入 token 数量都会比较大。但是不需要担心,大部分厂商都实现了 KV Cache,对相同前缀的输入存在缓存,缓存命中部分的费用开销是显著低于普通的 输入输出 token 价格的。OpenMCP 在每个回答的下面都表明了当次请求的 输入 token,输出 token,总 token 和 缓存命中率。
|
||||||
|
|
||||||
|
其中
|
||||||
|
|
||||||
|
- 「总 token」 = 「输入 token」 + 「输出 token」
|
||||||
|
|
||||||
|
- 「缓存命中率」 = 「缓存命令的 token」 / 「输入 token」
|
||||||
|
|
||||||
|
> 没错,缓存命中率 是对于输入 token 的概念,输出 token 是没有 缓存命中率这个说法的。
|
||||||
|
|
||||||
|
在后续的开发中,你可以根据这些信息来针对性地对你的服务或者 prompt 进行调优。
|
||||||
|
|
||||||
|
### 完成一个真正的天气预报吧!
|
||||||
|
|
||||||
|
当然,这些代码也非常简单,直接让大模型生成就行了(其实大模型是无法生成免 API 的 python 获取天气的代码的,我是直接让大模型把我个人网站上天气预报的 go 函数翻译了一下)
|
||||||
|
|
||||||
|
我直接把函数贴上来了:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
from typing import NamedTuple, Optional
|
||||||
|
|
||||||
|
class CityWeather(NamedTuple):
|
||||||
|
city_name_en: str
|
||||||
|
city_name_cn: str
|
||||||
|
city_code: str
|
||||||
|
temp: str
|
||||||
|
wd: str
|
||||||
|
ws: str
|
||||||
|
sd: str
|
||||||
|
aqi: str
|
||||||
|
weather: str
|
||||||
|
|
||||||
|
def get_city_weather_by_city_name(city_code: str) -> Optional[CityWeather]:
|
||||||
|
"""根据城市名获取天气信息"""
|
||||||
|
|
||||||
|
if not city_code:
|
||||||
|
print(f"找不到{city_code}对应的城市")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 构造请求URL
|
||||||
|
url = f"http://d1.weather.com.cn/sk_2d/{city_code}.html"
|
||||||
|
|
||||||
|
# 设置请求头
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.0.0",
|
||||||
|
"Host": "d1.weather.com.cn",
|
||||||
|
"Referer": "http://www.weather.com.cn/"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 发送HTTP请求
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# 解析JSON数据
|
||||||
|
# 解析JSON数据前先处理编码问题
|
||||||
|
content = response.text.encode('latin1').decode('unicode_escape')
|
||||||
|
json_start = content.find("{")
|
||||||
|
json_str = content[json_start:]
|
||||||
|
|
||||||
|
weather_data = json.loads(json_str)
|
||||||
|
|
||||||
|
# 构造返回对象
|
||||||
|
return CityWeather(
|
||||||
|
city_name_en=weather_data.get("nameen", ""),
|
||||||
|
city_name_cn=weather_data.get("cityname", "").encode('latin1').decode('utf-8'),
|
||||||
|
city_code=weather_data.get("city", ""),
|
||||||
|
temp=weather_data.get("temp", ""),
|
||||||
|
wd=weather_data.get("wd", "").encode('latin1').decode('utf-8'),
|
||||||
|
ws=weather_data.get("ws", "").encode('latin1').decode('utf-8'),
|
||||||
|
sd=weather_data.get("sd", ""),
|
||||||
|
aqi=weather_data.get("aqi", ""),
|
||||||
|
weather=weather_data.get("weather", "").encode('latin1').decode('utf-8')
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取天气信息失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
|
mcp = FastMCP('weather', version="0.0.1")
|
||||||
|
|
||||||
|
@mcp.tool(
|
||||||
|
name='get_weather_by_city_code',
|
||||||
|
description='根据城市天气预报的城市编码 (int),获取指定城市的天气信息'
|
||||||
|
)
|
||||||
|
def get_weather_by_code(city_code: int) -> str:
|
||||||
|
"""模拟天气查询协议,返回格式化字符串"""
|
||||||
|
city_weather = get_city_weather_by_city_name(city_code)
|
||||||
|
return str(city_weather)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
这里有几个点一定要注意:
|
||||||
|
|
||||||
|
1. 如果你的输入参数是数字,就算是城市编码这种比较长的数字,请一定定义成 int,因为 mcp 底层的是要走 JSON 正反序列化的,而 "114514" 这样的字符串会被 JSON 反序列化成 114514,而不是 "114514" 这个字符串。你实在要用 str 来表示一个很长的数字,那么就在前面加一个前缀,比如 "code-114514",避免被反序列化成数字,从而触发 mcp 内部的 type check error
|
||||||
|
2. tool 的 name 请按照 python 的变量命名要求进行命名,否则部分大模型厂商会给你报错。
|
||||||
|
|
||||||
|
好,我们先测试一下:
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-d2dbe925010b676482ee57258c14fca7_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
可以看到,我们的天气查询工具已经可以正常工作了。
|
||||||
|
|
||||||
|
那么接下来,我们就可以把这个工具注入到大模型中了。点击 「交互测试」,只激活当前这个工具,然后询问大模型:
|
||||||
|
```
|
||||||
|
请问杭州的天气是多少?
|
||||||
|
```
|
||||||
|
|
||||||
|
<div align=center>
|
||||||
|
<img src="https://picx.zhimg.com/80/v2-e581c6461190b358adda50ce83633520_1440w.png" style="width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
完美!
|
||||||
|
|
||||||
|
如此,我们便完成了一个天气查询工具的开发。并且轻松地注入到了我们的大模型中。在实际提供商业级部署方案的时候,虽然 mcp 目前的 stdio 冷启动速度足够快,但是考虑到拓展性等多方面因素,SSE 还是我们首选的连接方案,关于 SSE 的使用,我们下期再聊。
|
||||||
|
|
||||||
|
OpenMCP 开源链接:https://github.com/LSTM-Kirigaya/openmcp-client
|
0
plugin-tutorial/examples/sse-oauth2.md
Normal file
0
plugin-tutorial/faq/help.md
Normal file
BIN
plugin-tutorial/images/github-release.png
Normal file
After Width: | Height: | Size: 228 KiB |
BIN
plugin-tutorial/images/inspector.png
Normal file
After Width: | Height: | Size: 389 KiB |
BIN
plugin-tutorial/images/openmcp.png
Normal file
After Width: | Height: | Size: 624 KiB |
BIN
plugin-tutorial/images/vscode-plugin-market-install-from.png
Normal file
After Width: | Height: | Size: 213 KiB |
BIN
plugin-tutorial/images/vscode-plugin-market.png
Normal file
After Width: | Height: | Size: 467 KiB |
38
plugin-tutorial/index.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
# OpenMCP 概述
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
在正式开始 OpenMCP 的学习之前,我们强烈推荐您先了解一下 MCP 的基本概念:[Agent 时代基础设施 | MCP 协议介绍](https://kirigaya.cn/blog/article?seq=299)
|
||||||
|
:::
|
||||||
|
|
||||||
|
## 什么是 OpenMCP
|
||||||
|
|
||||||
|
OpenMCP 是一个面向开发者的 MCP 调试器和 SDK,致力于降低 AI Agent 的全链路开发成本和开发人员的心智负担。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
OpenMCP 分为两个部分,但是本板块讲解的是 OpenMCP 调试器的部分的使用,这部分也被我们称为 OpenMCP Client。OpenMCP Client 的本体是一个可在类 vscode 编辑器上运行的插件。它兼容了目前 MCP 协议的全部特性,且提供了丰富的利用开发者使用的功能,可以作为 Claude Inspector 的上位进行使用。
|
||||||
|
|
||||||
|
:::info 类 vscode 编辑器 (VLE)
|
||||||
|
类 vscode 编辑器 (vscode-like editor,简称 VLE) 是指基于 Vscodium 内核开发的通用型代码编辑器,它们都能够兼容的大部分的vscode插件生态,并且具有类似 vscode 的功能(比如支持 LSP3.7 协议、拥有 remote ssh 进行远程开发的能力、拥有跨编辑器的配置文件)。
|
||||||
|
|
||||||
|
比较典型的 VLE 有:vscode, trae, cursor 和 vscodium 各类发行版本。
|
||||||
|
:::
|
||||||
|
|
||||||
|
## 什么是 Claude Inspector
|
||||||
|
|
||||||
|
Claude Inspector 是一款 Claude 官方(也就是 MCP 协议的提出者)发布的开源 MCP 调试器,开发者在开发完 MCP 服务器后,可以通过这款调试器来测试功能完整性。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
但是 Inspector 工具存在如下几个缺点:
|
||||||
|
|
||||||
|
- 使用麻烦:使用 Inspector 每次都需要通过 mcp dev 启动一个 web 前后端应用
|
||||||
|
- 功能少:Inspector 只提供了最为基础的 MCP 的 tool 等属性的调试。如果用户想要测试自己开发的 MCP 服务器在大模型的交互下如何,还需要连接进入 Claude Desktop 并重启客户端,对于连续调试场景,非常不方便。
|
||||||
|
- 存在部分 bug:对于 SSE 和 streamable http 等远程连接的场景,Inspector 存在已知 bug,这对真实工业级开发造成了极大的影响。
|
||||||
|
- 无法对调试内容进行保存和留痕:在大规模微服务 mcp 化的项目中,这非常重要。
|
||||||
|
- 无法同时调试多个 mcp 服务器:在进行 mcp 原子化横向拓展的场景中,这是一项必要的功能。
|
||||||
|
|
||||||
|
而 OpenMCP Client 被我们制作出来的一个原因就是为了解决 Inspector 上述的痛点,从而让 mcp 服务器的开发门槛更低,用户能够更加专注于业务本身。
|
0
plugin-tutorial/usage/connect-llm.md
Normal file
0
plugin-tutorial/usage/connect-mcp.md
Normal file
0
plugin-tutorial/usage/debug.md
Normal file
0
plugin-tutorial/usage/distribute-result.md
Normal file
0
plugin-tutorial/usage/test-with-llm.md
Normal file
86
preview/changelog.md
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
## [main] 0.1.1
|
||||||
|
- 修复 SSH 连接 Ubuntu 的情况下的部分 bug
|
||||||
|
- 修复 python 项目点击 openmcp 进行连接时,初始化参数错误的问题
|
||||||
|
- 取消 service 底层的 mcp 连接复用技术,防止无法刷新
|
||||||
|
- 修复连接后,可能无法在欢迎界面选择调试选项的 bug
|
||||||
|
|
||||||
|
## [main] 0.1.0
|
||||||
|
- 新特性:支持同时连入多个 mcp server
|
||||||
|
- 新特性:更新协议内容,支持 streamable http 协议,未来将逐步取代 SSE 的连接方式
|
||||||
|
- impl issue#16:对于 uv 创建的 py 项目进行特殊支持,自动初始化项目,并将 mcp 定向到 .venv/bin/mcp 中,不再需要用户全局安装 mcp
|
||||||
|
- 对于 npm 创建的 js/ts 项目进行特殊支持:自动初始化项目
|
||||||
|
- 去除了 websearch 的设置,增加了 parallel_tool_calls 的设置,parallel_tool_calls 默认为 true,代表 允许模型在单轮回复中调用多个工具
|
||||||
|
- 重构了 openmcp 连接模块的基础设施,基于新的技术设施实现了更加详细的连接模块的日志系统.
|
||||||
|
- impl issue#15:无法复制
|
||||||
|
- impl issue#14:增加日志清除按钮
|
||||||
|
|
||||||
|
## [main] 0.0.9
|
||||||
|
- 修复 0.0.8 引入的bug:system prompt 返回的是索引而非真实内容
|
||||||
|
- 测试新的发布管线
|
||||||
|
|
||||||
|
## [main] 0.0.8
|
||||||
|
- 大模型 API 测试时更加完整的报错
|
||||||
|
- 修复 0.0.7 引入的bug:修改对话无法发出
|
||||||
|
- 修复 bug:富文本编辑器粘贴文本会带样式
|
||||||
|
- 修复 bug:富文本编辑器发送前缀为空的字符会全部为空
|
||||||
|
- 修复 bug:流式传输进行 function calling 时,多工具的索引串流导致的 JSON Schema 反序列化失败
|
||||||
|
- 修复 bug:大模型返回大量重复错误信息
|
||||||
|
- 新特性:支持一次对话同时调用多个工具
|
||||||
|
- UI:优化代码高亮的滚动条
|
||||||
|
- 新特性:resources/list 协议的内容点击就会直接渲染,无需二次发送
|
||||||
|
- 新特性:resources prompts tools 的结果的 json 模式支持高亮
|
||||||
|
|
||||||
|
## [main] 0.0.7
|
||||||
|
- 优化页面布局,使得调试窗口可以显示更多内容
|
||||||
|
- 扩大默认的上下文长度 10 -> 20
|
||||||
|
- 增加「通用选项」 -> 「MCP工具最长调用时间 (sec)」
|
||||||
|
- 支持富文本输入框,现在可以将 prompt 和 resource 嵌入到输入框中 进行 大规模 prompt engineering 调试工作了
|
||||||
|
|
||||||
|
## [main] 0.0.6
|
||||||
|
- 修复部分因为服务器名称特殊字符而导致的保存实效的错误
|
||||||
|
- 插件模式下,左侧管理面板中的「MCP连接(工作区)」视图可以进行增删改查了
|
||||||
|
- 新增「安装的 MCP 服务器」,用于安装全局范围的 mcp server
|
||||||
|
- 增加引导页面
|
||||||
|
- 修复无法进行离线 OCR 的问题
|
||||||
|
- 修复全局安装的 mcp 服务器 name 更新的问题
|
||||||
|
|
||||||
|
## [main] 0.0.5
|
||||||
|
- 支持对已经打开过的文件项目进行管理
|
||||||
|
- 支持对用户对应服务器的调试工作内容进行保存
|
||||||
|
- 支持连续工具调用和错误警告的显示
|
||||||
|
- 实现小型本地对象数据库,用于对对话产生的多媒体进行数据持久化
|
||||||
|
- 支持对于调用结果进行一键复现
|
||||||
|
- 支持对中间结果进行修改
|
||||||
|
- 支持 system prompt 的保存和修改
|
||||||
|
|
||||||
|
## [main] 0.0.4
|
||||||
|
- 修复选择模型后点击确认跳转回 deepseek 的 bug
|
||||||
|
- 修复 mcp 项目初始化点击工具全部都是空的 bug
|
||||||
|
- 修复无法重新连接的 bug
|
||||||
|
- 支持自定义第三方 openai 兼容的模型服务
|
||||||
|
|
||||||
|
## [main] 0.0.3
|
||||||
|
|
||||||
|
- 增加每一条信息的成本统计信息
|
||||||
|
- 修复初始化页面路由不为 debug 导致页面空白的 bug
|
||||||
|
|
||||||
|
## [main] 0.0.2
|
||||||
|
|
||||||
|
- 优化页面布局
|
||||||
|
- 解决更新标签页后打开无法显示的 bug
|
||||||
|
- 解决不如输入组件按下回车直接黑屏的 bug
|
||||||
|
- 更加完整方便的开发脚本
|
||||||
|
|
||||||
|
## [main] 0.0.1
|
||||||
|
|
||||||
|
- 完成 openmcp 的基础 inspector 功能
|
||||||
|
- 完成配置加载,保存,大模型设置
|
||||||
|
- 完成标签页自动保存
|
||||||
|
- 完成大模型对话窗口和工具调用
|
||||||
|
- 完成对 vscode 和 trae 的支持
|
24
preview/channel.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
# 资源频道
|
||||||
|
|
||||||
|
## 资源
|
||||||
|
|
||||||
|
[MCP 系列视频教程(正在施工中)](https://www.bilibili.com/video/BV1zYGozgEHc)
|
||||||
|
|
||||||
|
[锦恢的 mcp 系列博客](https://kirigaya.cn/blog/search?q=mcp)
|
||||||
|
|
||||||
|
[OpenMCP 官方文档](https://kirigaya.cn/openmcp/plugin-tutorial)
|
||||||
|
|
||||||
|
[openmcp-sdk 官方文档](https://kirigaya.cn/openmcp/sdk-tutorial)
|
||||||
|
|
||||||
|
## 频道
|
||||||
|
|
||||||
|
[OpenMCP 正式级技术组](https://qm.qq.com/cgi-bin/qm/qr?k=C6ZUTZvfqWoI12lWe7L93cWa1hUsuVT0&jump_from=webapi&authKey=McW6B1ogTPjPDrCyGttS890tMZGQ1KB3QLuG4aqVNRaYp4vlTSgf2c6dMcNjMuBD)
|
||||||
|
|
||||||
|
[OpenMCP Discord 频道](https://discord.gg/SKTZRf6NzU)
|
||||||
|
|
||||||
|
[OpenMCP 源代码](https://github.com/LSTM-Kirigaya/openmcp-client)
|
||||||
|
|
||||||
|
[OpenMCP 文档仓库](https://github.com/LSTM-Kirigaya/openmcp-document)
|
59
preview/contributors.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
layout: page
|
||||||
|
---
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
VPTeamPage,
|
||||||
|
VPTeamPageTitle,
|
||||||
|
VPTeamMembers
|
||||||
|
} from 'vitepress/theme'
|
||||||
|
|
||||||
|
const members = [
|
||||||
|
{
|
||||||
|
avatar: 'https://pic1.zhimg.com/v2-b4251de7d2499e942c7ebf447a90d2eb_xll.jpg?source=32738c0c',
|
||||||
|
name: 'LSTM-Kirigaya (锦恢)',
|
||||||
|
title: 'Creator & Developer',
|
||||||
|
links: [
|
||||||
|
{ icon: 'github', link: 'https://github.com/LSTM-Kirigaya' },
|
||||||
|
{ icon: 'zhihu', link: 'https://www.zhihu.com/people/can-meng-zhong-de-che-xian' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'https://avatars.githubusercontent.com/u/55867654?v=4',
|
||||||
|
name: 'li1553770945 (Li Yaning)',
|
||||||
|
title: 'Creator & Developer',
|
||||||
|
links: [
|
||||||
|
{ icon: 'github', link: 'https://github.com/li1553770945' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'https://avatars.githubusercontent.com/u/8943691?v=4',
|
||||||
|
name: 'appli456',
|
||||||
|
title: 'Developer',
|
||||||
|
links: [
|
||||||
|
{ icon: 'github', link: 'https://github.com/appli456' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'https://avatars.githubusercontent.com/u/115577936?v=4',
|
||||||
|
name: 'AmeSoraQwQ (AmeZora)',
|
||||||
|
title: 'Creator & Operation',
|
||||||
|
links: [
|
||||||
|
{ icon: 'github', link: 'https://github.com/AmeSoraQwQ' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<VPTeamPage>
|
||||||
|
<VPTeamPageTitle>
|
||||||
|
<template #title>
|
||||||
|
OpenMCP 贡献者列表
|
||||||
|
</template>
|
||||||
|
<template #lead>
|
||||||
|
OpenMCP 是一个非盈利的开源项目,它由对编程和AI技术热爱的开发者共同开发。我们欢迎任何有兴趣参与的开发者加入我们的项目中,一起努力提高AI技术的应用水平。
|
||||||
|
</template>
|
||||||
|
</VPTeamPageTitle>
|
||||||
|
<VPTeamMembers :members />
|
||||||
|
</VPTeamPage>
|
20
preview/join.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# 参与 OpenMCP 开发
|
||||||
|
|
||||||
|
|
||||||
|
## 想要参与开发,如果联系我们?
|
||||||
|
|
||||||
|
如果你也想要参与 OpenMCP 的开发,你可以通过如下的方式联系到我们:
|
||||||
|
|
||||||
|
- <a href="https://qm.qq.com/cgi-bin/qm/qr?k=C6ZUTZvfqWoI12lWe7L93cWa1hUsuVT0&jump_from=webapi&authKey=McW6B1ogTPjPDrCyGttS890tMZGQ1KB3QLuG4aqVNRaYp4vlTSgf2c6dMcNjMuBD" target="_blank">加入 OpenMCP正式级技术组</a> 来和我们直接讨论。
|
||||||
|
- 联系锦恢的邮箱 1193466151@qq.com
|
||||||
|
- 提交 [「Feature Request」类型的 issue](https://github.com/LSTM-Kirigaya/openmcp-client/issues) 或者 fork 代码后 [提交 PR](https://github.com/LSTM-Kirigaya/openmcp-client/pulls)
|
||||||
|
|
||||||
|
## 我能为 OpenMCP 做些什么?
|
||||||
|
|
||||||
|
虽然 OpenMCP 本体看起来技术乱七八糟,但是实际上,阁下可以为 OpenMCP 做的事情远比您想象的多。
|
||||||
|
|
||||||
|
- 通过提交 PR 贡献代码或者修复 bug,如果您愿意的话。
|
||||||
|
- 为我们的项目设计新的功能,这未必一定需要阁下写代码,只是在 [MVP 需求规划](https://github.com/LSTM-Kirigaya/openmcp-client?tab=readme-ov-file#%E9%9C%80%E6%B1%82%E8%A7%84%E5%88%92) 中提出有意义的功能也是很不错的贡献。
|
||||||
|
- 通过 OpenMCP 来完成不同的 agent 开发的例子或者打磨新的开发 AI Agent 的方法论。在征得阁下本人同意后,我们将会将你的教程整合到这个网站中。
|
||||||
|
|
||||||
|
通过向 OpenMCP 贡献以上内容或是其他,阁下将能成为 OpenMCP 的贡献者。
|
40
scripts/update-icon.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import requests as r
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
# 下载 压缩包
|
||||||
|
headers = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36',
|
||||||
|
'Cookie': 'xlly_s=1; cna=w4NlIMtvvVIBASQIgABnZxfD; ctoken=JAiIJ8vTLxVkJt4qnZXDbFu9; EGG_SESS_ICONFONT=Hu68kBY7XO7C6Udp3T99M1asKmUZ0gxjps8xjTrjx4aHaXwoIsDX25rpXZ2zp9tczibClyXdTQqv_kqXliYYccYUbU71pFxGJk2xcwvM-zixNt2D1jY18fYU0XW7DKzBelbgH7j20AOrp4NLnMCAeGGF8hsAMlq5eWwF7izXpY3df-y9RcoBvEmhZuOznUH_DidmM3uCXNeIeXB5w23A_FKndHS05gh2an7VfBF_MsfGIt1-j1XoeoDfbGhU2if_M9__TVNxMUtQQA_geOKrz1NByAcdO-kMa_ZXF40_Loc=; u=10114852; u.sig=mv5vi-TPPlhvQJi2PMIC4VoPpD03Wc9UykMTMiG6ElA; iconfont_has_read_tip=1; tfstk=gjCqKobIJCjW5bf4jUAZUBhcX1OvtIqCs1t6SNbMlnxmGjiGUis9cda9GO8M4Gp6mZwAQCShYnbf5lhZSIC5hftbDC7GACrQAWNCkZdJskZBiNwwJBYpSqm6mQ0k1F-fVxgPkZdty4igdTbxbcWuRZAGjQvk7FOMoFcgz4YBqjYiiFmuzFKksEAinLVkSFiMjjjGrz89qhAMnffPoNcyRK4a-KNkQlOJ3HbD4X7RaE2DYWKosrCkua-hoAhis_8231EQoCOXstbf4p64YjROo9I9-im00U5V81Whq7G2_aXh_Qfuq4tcpa5wGs4_iQfV0svPjVE9y9TF7p67Sv-5IaCyUsyIWUCfSspBtWnDfT_F_F5TX7SFoiWHKsmV4Zi9rSNR6toiQKYJzHazzEw6XK1Rl5FjBApk9U-QlEMtBKxBzHacIAH9H28yArTf.',
|
||||||
|
'Pragma': 'no-cache',
|
||||||
|
'sec-fetch-mode': 'navigate'
|
||||||
|
}
|
||||||
|
|
||||||
|
url = 'https://www.iconfont.cn/api/project/download.zip?spm=a313x.manage_type_myprojects.i1.d7543c303.171f3a81ARDpip&pid=4933953&ctoken=Z1wlDrxyuJ5o_GLSW5xW8QXJ'
|
||||||
|
res = r.get(url, headers=headers)
|
||||||
|
|
||||||
|
if res.status_code:
|
||||||
|
with open('./scripts/tmp.zip', 'wb') as fp:
|
||||||
|
fp.write(res.content)
|
||||||
|
|
||||||
|
# 解压文件
|
||||||
|
with zipfile.ZipFile('./scripts/tmp.zip', 'r') as zipf:
|
||||||
|
zipf.extractall('./scripts/tmp')
|
||||||
|
|
||||||
|
# 将文件搬运至工作区,我的 css 全放在 public 下面了,你的视情况而定
|
||||||
|
for parent, _, files in os.walk('./scripts/tmp'):
|
||||||
|
for file in files:
|
||||||
|
filepath = os.path.join(parent, file)
|
||||||
|
if file.startswith('demo'):
|
||||||
|
continue
|
||||||
|
if file.endswith('.css'):
|
||||||
|
content = open(filepath, 'r', encoding='utf-8').read().replace('font-size: 16px;', '')
|
||||||
|
open(filepath, 'w', encoding='utf-8').write(content)
|
||||||
|
shutil.move(filepath, os.path.join('./.vitepress/theme', file))
|
||||||
|
elif file.endswith('.woff2'):
|
||||||
|
shutil.move(filepath, os.path.join('./.vitepress/theme', file))
|
||||||
|
|
||||||
|
# 删除压缩包和解压区域
|
||||||
|
os.remove('./scripts/tmp.zip')
|
||||||
|
shutil.rmtree('./scripts/tmp')
|
107
sdk-tutorial/index.md
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
|
||||||
|
|
||||||
|
# 介绍 & 安装
|
||||||
|
|
||||||
|
## 什么是 openmcp-sdk.js
|
||||||
|
|
||||||
|
OpenMCP Client 提供了一体化的 MCP 调试解决方案,这很好,但是,还是不够有趣。
|
||||||
|
|
||||||
|
因为,我们总是希望可以把做好的 mcp 搞一个可以直接分发的 app 或者扔到服务器上做成一个函数服务或者微服务。而 OpenMCP Client 把和大模型交互,使用工具的这套逻辑全部放到了前端,导致我们如果想要把 mcp 做成一个和大模型绑定的独立应用或者服务,困难重重。
|
||||||
|
|
||||||
|
这个时候,openmcp-sdk.js 就提供了一种轻量级解决方案。它是一个 nodejs 的库,可以让您通过 nodejs 将写好的 mcp 和调试好的流程无缝部署成一个 agent。
|
||||||
|
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
```[npm]
|
||||||
|
npm install openmcp-sdk
|
||||||
|
```
|
||||||
|
|
||||||
|
```[yarn]
|
||||||
|
yarn add openmcp-sdk
|
||||||
|
```
|
||||||
|
|
||||||
|
```[pnpm]
|
||||||
|
pnpm add openmcp-sdk
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
下面是一个最小例程:
|
||||||
|
|
||||||
|
文件名:main.ts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { TaskLoop } from 'openmcp-sdk/task-loop';
|
||||||
|
import { TaskLoopAdapter } from 'openmcp-sdk/service';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// 创建适配器,负责通信和 mcp 连接
|
||||||
|
const adapter = new TaskLoopAdapter();
|
||||||
|
|
||||||
|
// 连接 mcp 服务器
|
||||||
|
await adapter.connectMcpServer({
|
||||||
|
connectionType: 'STDIO',
|
||||||
|
command: 'node',
|
||||||
|
args: [
|
||||||
|
'~/projects/mcp/servers/src/puppeteer/dist/index.js'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取工具列表
|
||||||
|
const tools = await adapter.listTools();
|
||||||
|
|
||||||
|
// 创建事件循环驱动器
|
||||||
|
const taskLoop = new TaskLoop({ adapter });
|
||||||
|
|
||||||
|
// 配置改次事件循环使用的大模型
|
||||||
|
taskLoop.setLlmConfig({
|
||||||
|
id: 'deepseek',
|
||||||
|
baseUrl: 'https://api.deepseek.com/v1',
|
||||||
|
userToken: process.env['DEEPSEEK_API_TOKEN'],
|
||||||
|
userModel: 'deepseek-chat'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建当前事件循环对应的上下文,并且配置当前上下文的设置
|
||||||
|
const storage = {
|
||||||
|
messages: [],
|
||||||
|
settings: {
|
||||||
|
temperature: 0.7,
|
||||||
|
enableTools: tools,
|
||||||
|
systemPrompt: 'you are a clever bot',
|
||||||
|
contextLength: 20
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 本次发出的问题
|
||||||
|
const message = 'hello world';
|
||||||
|
|
||||||
|
// 事件循环结束的句柄
|
||||||
|
taskLoop.registerOnDone(() => {
|
||||||
|
console.log('taskLoop done');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 事件循环每一次 epoch 开始的句柄
|
||||||
|
taskLoop.registerOnError((error) => {
|
||||||
|
console.log('taskLoop error', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 事件循环出现 error 时的句柄(出现 error 不一定会停止事件循环)
|
||||||
|
taskLoop.registerOnEpoch(() => {
|
||||||
|
console.log('taskLoop epoch');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开启事件循环
|
||||||
|
await taskLoop.start(storage, message);
|
||||||
|
|
||||||
|
// 打印上下文,最终的回答在 messages.at(-1) 中
|
||||||
|
console.log(storage.messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
```
|
||||||
|
|
||||||
|
star 我们的项目:https://github.com/LSTM-Kirigaya/openmcp-client
|