diff --git a/.vitepress/config.mts b/.vitepress/config.mts index ac4ff1f..2be0fbb 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -16,16 +16,57 @@ export default defineConfig({ // https://vitepress.dev/reference/default-theme-config nav: [ { text: '首页', link: '/' }, - { text: '教程', link: '/plugin-tutorial' }, + { + 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: [ + text: 'Prewiew 0.1.0', + items: [ { component: 'KNavItem', props: { title: '更新日志', description: '查看项目的更新历史记录', - icon: '/images/icons/monitor.svg', + icon: 'a-yusuan2', link: '/preview/changelog' } }, @@ -34,16 +75,25 @@ export default defineConfig({ props: { title: '参与 OpenMCP', description: '了解如何参与 OpenMCP 项目的开发和维护', - icon: '/images/icons/group.svg', + icon: 'shujuzhongxin', link: '/preview/join' } }, + { + component: 'KNavItem', + props: { + title: 'OpenMCP 贡献者列表', + description: '关于参与 OpenMCP 的贡献者们', + icon: 'heike', + link: '/preview/contributors' + } + }, { component: 'KNavItem', props: { title: '资源频道', description: '获取项目相关的资源和信息', - icon: '/images/icons/ai.svg', + icon: 'xinxiang', link: '/preview/channel' } } @@ -51,6 +101,45 @@ export default defineConfig({ }, ], + sidebar: { + '/plugin-tutorial/': [ + { + text: '简介', + 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' }, + ] + } + ] + }, + socialLinks: [ { icon: 'github', link: 'https://github.com/LSTM-Kirigaya/openmcp-client' }, { icon: customIcons.share, link: 'https://kirigaya.cn/home' }, diff --git a/.vitepress/theme/components/home/TwoSideLayout.vue b/.vitepress/theme/components/home/TwoSideLayout.vue index f4053bd..a9f8d1f 100644 --- a/.vitepress/theme/components/home/TwoSideLayout.vue +++ b/.vitepress/theme/components/home/TwoSideLayout.vue @@ -1,6 +1,6 @@ diff --git a/.vitepress/theme/iconfont.css b/.vitepress/theme/iconfont.css new file mode 100644 index 0000000..5a0287d --- /dev/null +++ b/.vitepress/theme/iconfont.css @@ -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"; +} + diff --git a/.vitepress/theme/iconfont.woff2 b/.vitepress/theme/iconfont.woff2 new file mode 100644 index 0000000..bbc26f3 Binary files /dev/null and b/.vitepress/theme/iconfont.woff2 differ diff --git a/.vitepress/theme/index.mts b/.vitepress/theme/index.mts index bf401d4..4d50acf 100644 --- a/.vitepress/theme/index.mts +++ b/.vitepress/theme/index.mts @@ -12,6 +12,7 @@ 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, diff --git a/.vitepress/theme/style.css b/.vitepress/theme/style.css index d0acdf1..f1577af 100644 --- a/.vitepress/theme/style.css +++ b/.vitepress/theme/style.css @@ -44,30 +44,30 @@ * -------------------------------------------------------------------------- */ :root { - --vp-c-default-1: var(--vp-c-gray-1); - --vp-c-default-2: var(--vp-c-gray-2); - --vp-c-default-3: var(--vp-c-gray-3); - --vp-c-default-soft: var(--vp-c-gray-soft); + --vp-c-default-1: var(--vp-c-gray-1); + --vp-c-default-2: var(--vp-c-gray-2); + --vp-c-default-3: var(--vp-c-gray-3); + --vp-c-default-soft: var(--vp-c-gray-soft); - --vp-c-brand-1: var(--vp-c-indigo-1); - --vp-c-brand-2: var(--vp-c-indigo-2); - --vp-c-brand-3: var(--vp-c-indigo-3); - --vp-c-brand-soft: var(--vp-c-indigo-soft); + --vp-c-brand-1: #d8a8e7; /* 较深的变体 */ + --vp-c-brand-2: #C285D6; /* 基底颜色 */ + --vp-c-brand-3: #a368b8; /* 较浅的变体 */ + --vp-c-brand-soft: rgba(194, 133, 214, 0.1); /* 半透明的柔和版本 */ - --vp-c-tip-1: var(--vp-c-brand-1); - --vp-c-tip-2: var(--vp-c-brand-2); - --vp-c-tip-3: var(--vp-c-brand-3); - --vp-c-tip-soft: var(--vp-c-brand-soft); + --vp-c-tip-1: var(--vp-c-brand-1); + --vp-c-tip-2: var(--vp-c-brand-2); + --vp-c-tip-3: var(--vp-c-brand-3); + --vp-c-tip-soft: var(--vp-c-brand-soft); - --vp-c-warning-1: var(--vp-c-yellow-1); - --vp-c-warning-2: var(--vp-c-yellow-2); - --vp-c-warning-3: var(--vp-c-yellow-3); - --vp-c-warning-soft: var(--vp-c-yellow-soft); + --vp-c-warning-1: var(--vp-c-yellow-1); + --vp-c-warning-2: var(--vp-c-yellow-2); + --vp-c-warning-3: var(--vp-c-yellow-3); + --vp-c-warning-soft: var(--vp-c-yellow-soft); - --vp-c-danger-1: var(--vp-c-red-1); - --vp-c-danger-2: var(--vp-c-red-2); - --vp-c-danger-3: var(--vp-c-red-3); - --vp-c-danger-soft: var(--vp-c-red-soft); + --vp-c-danger-1: var(--vp-c-red-1); + --vp-c-danger-2: var(--vp-c-red-2); + --vp-c-danger-3: var(--vp-c-red-3); + --vp-c-danger-soft: var(--vp-c-red-soft); } /** @@ -75,15 +75,15 @@ * -------------------------------------------------------------------------- */ :root { - --vp-button-brand-border: transparent; - --vp-button-brand-text: var(--vp-c-white); - --vp-button-brand-bg: var(--vp-c-brand-3); - --vp-button-brand-hover-border: transparent; - --vp-button-brand-hover-text: var(--vp-c-white); - --vp-button-brand-hover-bg: var(--vp-c-brand-2); - --vp-button-brand-active-border: transparent; - --vp-button-brand-active-text: var(--vp-c-white); - --vp-button-brand-active-bg: var(--vp-c-brand-1); + --vp-button-brand-border: transparent; + --vp-button-brand-text: var(--vp-c-white); + --vp-button-brand-bg: var(--vp-c-brand-3); + --vp-button-brand-hover-border: transparent; + --vp-button-brand-hover-text: var(--vp-c-white); + --vp-button-brand-hover-bg: var(--vp-c-brand-2); + --vp-button-brand-active-border: transparent; + --vp-button-brand-active-text: var(--vp-c-white); + --vp-button-brand-active-bg: var(--vp-c-brand-1); } /** @@ -91,31 +91,27 @@ * -------------------------------------------------------------------------- */ :root { - --vp-home-hero-name-color: transparent; - --vp-home-hero-name-background: -webkit-linear-gradient( - 120deg, - #bd34fe 30%, - #41d1ff - ); + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: -webkit-linear-gradient(120deg, + #bd34fe 30%, + #41d1ff); - --vp-home-hero-image-background-image: linear-gradient( - -45deg, - #bd34fe 50%, - #47caff 50% - ); - --vp-home-hero-image-filter: blur(44px); + --vp-home-hero-image-background-image: linear-gradient(-45deg, + #bd34fe 50%, + #47caff 50%); + --vp-home-hero-image-filter: blur(44px); } @media (min-width: 640px) { - :root { - --vp-home-hero-image-filter: blur(56px); - } + :root { + --vp-home-hero-image-filter: blur(56px); + } } @media (min-width: 960px) { - :root { - --vp-home-hero-image-filter: blur(68px); - } + :root { + --vp-home-hero-image-filter: blur(68px); + } } /** @@ -123,15 +119,15 @@ * -------------------------------------------------------------------------- */ :root { - --vp-custom-block-tip-border: transparent; - --vp-custom-block-tip-text: var(--vp-c-text-1); - --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-border: transparent; + --vp-custom-block-tip-text: var(--vp-c-text-1); + --vp-custom-block-tip-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); + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: -webkit-linear-gradient(120deg, #bd34fe, #41d1ff); } /** @@ -139,22 +135,59 @@ * -------------------------------------------------------------------------- */ .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; + display: unset !important; } .VPHero .image-container img { - box-shadow: unset !important; - max-width: 290px !important; - max-height: 290px !important; + 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; + 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); +} \ No newline at end of file diff --git a/images/icons/ai.svg b/images/icons/ai.svg deleted file mode 100644 index 5386a79..0000000 --- a/images/icons/ai.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/images/icons/group.svg b/images/icons/group.svg deleted file mode 100644 index ab4c8b4..0000000 --- a/images/icons/group.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/images/icons/monitor.svg b/images/icons/monitor.svg deleted file mode 100644 index f605195..0000000 --- a/images/icons/monitor.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/index.md b/index.md index 6fc8849..c3dd313 100644 --- a/index.md +++ b/index.md @@ -13,7 +13,7 @@ hero: actions: - theme: brand text: OpenMCP 插件 - link: /markdown-examples + link: /plugin-tutorial - theme: alt text: openmcp-sdk link: /sdk-tutorial @@ -89,3 +89,4 @@ features: /> + \ No newline at end of file diff --git a/plugin-tutorial/acquire-openmcp.md b/plugin-tutorial/acquire-openmcp.md new file mode 100644 index 0000000..207c5c9 --- /dev/null +++ b/plugin-tutorial/acquire-openmcp.md @@ -0,0 +1,51 @@ +--- +layout: doc +--- + + +# 获取 OpenMCP + +## 在插件商城中安装 OpenMCP + +你可以在主流 VLE 的插件商城直接获取 OpenMCP 插件。比如在 vscode 中,点击左侧的插件商城,然后在搜索框中输入 `OpenMCP` 即可找到 OpenMCP 插件。 + +![vscode 插件商城](./images/vscode-plugin-market.png) + +## 离线安装 + +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 压缩包 + +![github release](./images/github-release.png) + +除此之外,您还可以通过如下的商城网页来获取最新的 openmcp 的 vsix + +- https://open-vsx.org/extension/kirigaya/openmcp +- https://marketplace.visualstudio.com/items?itemName=kirigaya.openmcp + +点击 vsix 后缀名的文件下载,下载完成后,您就可以直接安装它了。在 VLE 中安装外部的 vsix 文件有两种方法。 + +### 方法一:在 VLE 中安装 + +VLE 的插件商城页面有一个三个点的按钮,点击它后,你能看到下面这个列表中被我标红的按钮 + +![vscode 插件商城](./images/vscode-plugin-market-install-from.png) + +点击它后,找到刚刚下载的 vsix 文件,点击即可完成安装。 + +### 方法二:通过命令行 + +如果您的 VLE 是全局安装的,会自动存在一个命令行工具,此处以 vscode 为例子(trae 的命令为 trae),打开命令行,输入 + +```bash +code --install-extension /path/to/openmcp-0.1.1.vsix +``` + +/path/to/openmcp-0.1.1.vsix 代表你刚刚下载的 vsix 文件的绝对路径。这样也可以安装插件。 \ No newline at end of file diff --git a/plugin-tutorial/concept.md b/plugin-tutorial/concept.md new file mode 100644 index 0000000..43dec0d --- /dev/null +++ b/plugin-tutorial/concept.md @@ -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="", 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` 这个参数的图形化显示。 + +
+ +
+ +其实如你所见,一次普普通通调用涉及到的可调控参数是非常之多的。而在所有参数中,你可以注意到一个参数叫做 `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`,你可以通过输入参数运行来查看这个函数是否正常工作。(因为一般情况下的这个资源协议是会访问远程数据库或者微服务的) + +
+ +
+ +Prompts 端就比较简单了,直接输入预定义参数就能获取正常的返回结果。 + +
+ +
+ +Tools 端将会是我们后面调试的核心。在之前的章节我们讲过了,MCP 协议中的 Prompts 和 Resources 目前还没有被 openai 协议和各大 MCP 客户端广泛支持,因此,我们主要的服务端业务都应该是在写 tools。 + +我们此处提供的 tool 是实现一个简单的加法,它非常简单,我们输入 1 和 2 就可以直接看到结果是 3。我们后续会开发一个可以访问天气预报的 tool,那么到时候就非常需要一个这样的窗口来调试我们的天气信息获取是否正常了。 + +
+ +
+ +--- + +## 结余 + +这篇文章,我们简单了解了 MCP 内部的一些基本概念,我认为这些概念对于诸位开发一个 MCP 服务器是大有裨益的,所以我认为有必要先讲一讲。 + +下面的文章中,我将带领大家探索 MCP 的奇境,一个属于 AI Agent 的时代快要到来了。 + +--- + +## 挖坑 + +从上面的例子中,大家也能看出其实现在调试 MCP Server 的工具还不算齐全,所以我打算最近快速开发一款 vscode 插件,集合 Inspector 的所有功能和基础的大模型测试为一体。如果你开发了基本的网络和磁盘访问的 MCP Server,这个调试工具也可以当成一个 Manus 客户端进行把玩。 + +请大家期待吧! diff --git a/plugin-tutorial/examples/go-neo4j-sse.md b/plugin-tutorial/examples/go-neo4j-sse.md new file mode 100644 index 0000000..c1bb5b2 --- /dev/null +++ b/plugin-tutorial/examples/go-neo4j-sse.md @@ -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 +``` + +
+ +
+ +### 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 空着不填即可。 + +
+ +
+ +### 4.2 测试工具 + +第一次调试 mcp 服务器要做的事情一定是先调通 mcp tool,新建标签页,选择 tool,点击下图的工具,输入 `CALL db.labels() YIELD label RETURN label`,这个语句是用来列出所有节点类型的。如果输出下面的结果,说明当前的链路生效,没有问题。 + +
+ +
+ + +### 4.3 摸清大模型功能边界,用提示词来封装我们的知识 + +然后,让我们做点有趣的事情吧!我们接下来要测试一下大模型的能力边界,因为 neo4j 属于特种数据库,通用大模型不一定知道怎么用它。新建标签页,点击「交互测试」,我们先问一个简单的问题: + +``` +帮我找出最新的 10 条评论 +``` + +结果如下: + +
+ +
+ +可以看到,大模型查询的节点类型就是错误的,在我提供的例子中,代表评论的节点是 BlogComment,而不是 Comment。也就是说,大模型并不掌握进行数据库查询的通用方法论。这就是我们目前知道的它的能力边界。我们接下来要一步一步地注入我们的经验和知识,唔姆,通过 system prompt 来完成。 + +### 4.4 教大模型找数据库节点 + +好好想一下,作为工程师的我们是怎么知道评论的节点是 BlogComment?我们一般是通过罗列当前数据库的所有节点的类型来从命名中猜测的,比如,对于这个数据库,我一般会先输入如下的 cypher 查询: + +```sql +CALL db.labels() YIELD label RETURN label +``` + +它的输出就在 4.2 的图中,如果你的英文不错,也能看出来 BlogComment 大概率是代表博客评论的节点。好了,那么我们将这段方法论注入到 system prompt 中,从而封装我们的这层知识,点击下图的下方的按钮,进入到【系统提示词】: + +
+ +
+ + +新建提示词【neo4j】,输入: + +``` +你是一个善于进行neo4j查询的智能体,对于用户要求的查询请求,你并不一定知道对应的数据库节点是什么,这个时候,你需要先列出所有的节点类型,然后从中找到你认为最有可能是匹配用户询问的节点。比如用户问你要看符合特定条件的「文章」,你并不知道文章的节点类型是什么,这个时候,你就需要先列出所有的节点。 +``` + +点击保存,然后在【交互测试】中,重复刚才的问题: + +``` +帮我找出最新的 10 条评论 +``` + +大模型的回答如下: + +
+ +
+ +诶?怎么说,是不是好了很多了?大模型成功找到了 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篇文章。 +``` + +结果如下: + +
+ +
+ + +是不是很完美? + +通过使用 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 删除),询问结果如下: + +
+ +
+ + +可以看到,在没有 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 项目里面有) \ No newline at end of file diff --git a/plugin-tutorial/examples/java-es-http.md b/plugin-tutorial/examples/java-es-http.md new file mode 100644 index 0000000..e69de29 diff --git a/plugin-tutorial/examples/python-simple-stdio.md b/plugin-tutorial/examples/python-simple-stdio.md new file mode 100644 index 0000000..c52d588 --- /dev/null +++ b/plugin-tutorial/examples/python-simple-stdio.md @@ -0,0 +1,436 @@ +# python 实现天气信息 mcp 服务器 + +## hook + +等等,开始前,先让我们看一个小例子,假设我下周要去明日方舟锈影新生的漫展,所以我想要知道周六杭州的天气,于是我问大模型周六的天气,结果大模型给了我如下的回复: + +
+ +
+ +这可不行,相信朋友们也经常遇到过这样的情况,大模型总是会“授人以渔”,但是有的时候,我们往往就是想要直接知道最终结果,特别是一些无聊的生活琐事。 + +其实实现天气预报的程序也很多啦,那么有什么方法可以把写好的天气预报的程序接入大模型,让大模型告诉我们真实的天气情况,从而选择明天漫展的穿搭选择呢? + +如果直接写函数用 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 插件 + +
+ +
+ +先制作一个 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 了。 + +
+ +
+ +默认是以 STDIO 的方式启动,默认运行如下的命令: + +```bash +uv run mcp run <当前打开的 python 文件的相对路径> +``` + +所以你需要保证已经安装了 mcp 脚手架,也就是 `uv add mcp "mcp[cli]"`。 + +打开后第一件事就是先看左下角连接状态,确保是绿色的,代表当前 OpenMCP 和你的 MCP 服务器已经握手成功。 + +
+ +
+ +如果连接成功,此时连接上方还会显示你当前的 MCP 服务器的名字,光标移动上去还能看到版本号。这些信息由我们如下的代码定义: + +```python +mcp = FastMCP('锦恢的 MCP Server', version="11.45.14") +``` + +这在我们进行版本管理的时候会非常有用。请善用这套系统。 + + +如果连接失败,可以点击左侧工具栏的第二个按钮,进入连接控制台,查看错误信息,或是手动调整连接命令: + +
+ +
+ +### 初识 OpenMCP + +接下来,我来简单介绍一下 OpenMCP 的基本功能模块,如果一开始,你的屏幕里什么也没有,先点击上面的加号创建一个新的标签页,此处页面中会出现下图屏幕中的四个按钮 + +
+ +
+ +放大一点 + +
+ +
+ +前三个,资源、提词和工具,分别用于调试 MCP 中的三个对应项目,也就是 Resources,Prompts 和 Tools,这三个部分的使用,基本和 MCP 官方的 Inspector 工具是一样的,那是自然,我就照着这几个抄的,诶嘿。 + +
+ +
+ +然后第四个按钮「交互测试」,它是一个我开发的 MCP 客户端,其实就是一个对话窗口,你可以无缝衔接地直接在大模型中测试你当前的 MCP 服务器的功能函数。 + +
+ +
+ + +目前我暂时只支持 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,新建一个「工具」调试项目 + +
+ +
+ +然后此时,你在左侧的列表可以看到 weather 这个工具,选择它,然后在右侧的输入框中随便输入一些东西,按下回车(或者点击「运行」),你能看到如下的响应: + +
+ +
+ +看到我们函数 return 的字符串传过来了,说明没问题,链路通了。 + +### 交互测试 + +诶?我知道你编程很厉害,但是,在噼里啪啦快速写完天气预报爬虫前,我们现在看看我们要如何把已经写好的工具注入大模型对话中。为了使用大模型,我们需要先选择大模型和对应的 API,点击左侧工具栏的第三个按钮,进入 API 模块,选择你想要使用的大模型运营商、模型,填写 API token,然后点击下面的「保存」 + +
+ +
+ +再新建一个标签页,选择「交互测试」,此时,我们就可以直接和大模型对话了,我们先看看没有任何工具注入的大模型会如何回应天气预报的问题,点击最下侧工具栏从左往右第三个按钮,进入工具选择界面,选择「禁用所有工具」 + +
+ +
+ +点击「关闭」后,我们问大模型一个问题: + +``` +请问杭州的温度是多少? +``` + +
+ +
+ +可以看到,大模型给出了和文章开头一样的回答。非常敷衍,因为它确实无法知道。 + +此处,我们再单独打开「weather」工具: + +
+ +
+ +问出相同的问题: + +
+ +
+ +可以看到,大模型给出了回答是 25 度,还有一些额外的推导信息。 + +我们不妨关注一些细节,首先,大模型并不会直接回答问题,而是会先去调用 weather 这个工具,调用参数为: + +```json +{ + "city": "杭州" +} +``` + +然后,我们的 MCP 服务器给出了响应: + +``` +Weather in 杭州: Sunny, 25°C +``` + +从而,最终大模型才根据这些信息给出了最终的回答。也就是,这个过程我们实际调用了两次大模型的服务。而且可以看到两次调用的输入 token 数量都非常大,这是因为 OpenMCP 会将函数调用以 JSON Schema 的形式注入到请求参数中,weather 这个工具的 JSON Schema 如下图的右侧的 json 所示: + +
+ +
+ +然后支持 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 的变量命名要求进行命名,否则部分大模型厂商会给你报错。 + +好,我们先测试一下: + +
+ +
+ +可以看到,我们的天气查询工具已经可以正常工作了。 + +那么接下来,我们就可以把这个工具注入到大模型中了。点击 「交互测试」,只激活当前这个工具,然后询问大模型: +``` +请问杭州的天气是多少? +``` + +
+ +
+ +完美! + +如此,我们便完成了一个天气查询工具的开发。并且轻松地注入到了我们的大模型中。在实际提供商业级部署方案的时候,虽然 mcp 目前的 stdio 冷启动速度足够快,但是考虑到拓展性等多方面因素,SSE 还是我们首选的连接方案,关于 SSE 的使用,我们下期再聊。 + +OpenMCP 开源链接:https://github.com/LSTM-Kirigaya/openmcp-client \ No newline at end of file diff --git a/plugin-tutorial/examples/sse-oauth2.md b/plugin-tutorial/examples/sse-oauth2.md new file mode 100644 index 0000000..e69de29 diff --git a/plugin-tutorial/examples/typescript-crawl4ai-stdio.md b/plugin-tutorial/examples/typescript-crawl4ai-stdio.md new file mode 100644 index 0000000..e69de29 diff --git a/plugin-tutorial/faq/help.md b/plugin-tutorial/faq/help.md new file mode 100644 index 0000000..e69de29 diff --git a/plugin-tutorial/images/github-release.png b/plugin-tutorial/images/github-release.png new file mode 100644 index 0000000..f34ed8e Binary files /dev/null and b/plugin-tutorial/images/github-release.png differ diff --git a/plugin-tutorial/images/inspector.png b/plugin-tutorial/images/inspector.png new file mode 100644 index 0000000..442aef4 Binary files /dev/null and b/plugin-tutorial/images/inspector.png differ diff --git a/plugin-tutorial/images/openmcp.png b/plugin-tutorial/images/openmcp.png new file mode 100644 index 0000000..b732b99 Binary files /dev/null and b/plugin-tutorial/images/openmcp.png differ diff --git a/plugin-tutorial/images/vscode-plugin-market-install-from.png b/plugin-tutorial/images/vscode-plugin-market-install-from.png new file mode 100644 index 0000000..4b22fe2 Binary files /dev/null and b/plugin-tutorial/images/vscode-plugin-market-install-from.png differ diff --git a/plugin-tutorial/images/vscode-plugin-market.png b/plugin-tutorial/images/vscode-plugin-market.png new file mode 100644 index 0000000..0033ecd Binary files /dev/null and b/plugin-tutorial/images/vscode-plugin-market.png differ diff --git a/plugin-tutorial/index.md b/plugin-tutorial/index.md index 6bd8bb5..d1cd437 100644 --- a/plugin-tutorial/index.md +++ b/plugin-tutorial/index.md @@ -1,49 +1,38 @@ --- -outline: deep --- -# Runtime API Examples +# OpenMCP 概述 -This page demonstrates usage of some of the runtime APIs provided by VitePress. +:::warning +在正式开始 OpenMCP 的学习之前,我们强烈推荐您先了解一下 MCP 的基本概念:[Agent 时代基础设施 | MCP 协议介绍](https://kirigaya.cn/blog/article?seq=299) +::: -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: +## 什么是 OpenMCP -```md - +![](./images/openmcp.png) -## Results +OpenMCP 分为两个部分,但是本板块讲解的是 OpenMCP 调试器的部分的使用,这部分也被我们称为 OpenMCP Client。OpenMCP Client 的本体是一个可在类 vscode 编辑器上运行的插件。它兼容了目前 MCP 协议的全部特性,且提供了丰富的利用开发者使用的功能,可以作为 Claude Inspector 的上位进行使用。 -### Theme Data -
{{ theme }}
+:::info 类 vscode 编辑器 (VLE) +类 vscode 编辑器 (vscode-like editor,简称 VLE) 是指基于 Vscodium 内核开发的通用型代码编辑器,它们都能够兼容的大部分的vscode插件生态,并且具有类似 vscode 的功能(比如支持 LSP3.7 协议、拥有 remote ssh 进行远程开发的能力、拥有跨编辑器的配置文件)。 -### Page Data -
{{ page }}
+比较典型的 VLE 有:vscode, trae, cursor 和 vscodium 各类发行版本。 +::: -### Page Frontmatter -
{{ frontmatter }}
-``` +## 什么是 Claude Inspector - +![](./images/inspector.png) -## Results +但是 Inspector 工具存在如下几个缺点: -### Theme Data -
{{ theme }}
+- 使用麻烦:使用 Inspector 每次都需要通过 mcp dev 启动一个 web 前后端应用 +- 功能少:Inspector 只提供了最为基础的 MCP 的 tool 等属性的调试。如果用户想要测试自己开发的 MCP 服务器在大模型的交互下如何,还需要连接进入 Claude Desktop 并重启客户端,对于连续调试场景,非常不方便。 +- 存在部分 bug:对于 SSE 和 streamable http 等远程连接的场景,Inspector 存在已知 bug,这对真实工业级开发造成了极大的影响。 +- 无法对调试内容进行保存和留痕:在大规模微服务 mcp 化的项目中,这非常重要。 +- 无法同时调试多个 mcp 服务器:在进行 mcp 原子化横向拓展的场景中,这是一项必要的功能。 -### Page Data -
{{ page }}
- -### Page Frontmatter -
{{ frontmatter }}
- -## More - -Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata). +而 OpenMCP Client 被我们制作出来的一个原因就是为了解决 Inspector 上述的痛点,从而让 mcp 服务器的开发门槛更低,用户能够更加专注于业务本身。 \ No newline at end of file diff --git a/plugin-tutorial/usage/connect-llm.md b/plugin-tutorial/usage/connect-llm.md new file mode 100644 index 0000000..e69de29 diff --git a/plugin-tutorial/usage/connect-mcp.md b/plugin-tutorial/usage/connect-mcp.md new file mode 100644 index 0000000..e69de29 diff --git a/plugin-tutorial/usage/debug.md b/plugin-tutorial/usage/debug.md new file mode 100644 index 0000000..e69de29 diff --git a/plugin-tutorial/usage/distribute-result.md b/plugin-tutorial/usage/distribute-result.md new file mode 100644 index 0000000..e69de29 diff --git a/plugin-tutorial/usage/test-with-llm.md b/plugin-tutorial/usage/test-with-llm.md new file mode 100644 index 0000000..e69de29 diff --git a/preview/changelog.md b/preview/changelog.md index ff97b7c..1d664bf 100644 --- a/preview/changelog.md +++ b/preview/changelog.md @@ -1,3 +1,86 @@ --- -layout: page ---- \ No newline at end of file + +--- + +# 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 的支持 diff --git a/preview/channel.md b/preview/channel.md index ff97b7c..0a9ba36 100644 --- a/preview/channel.md +++ b/preview/channel.md @@ -1,3 +1,24 @@ --- -layout: page ---- \ No newline at end of file +--- + +# 资源频道 + +## 资源 + +[MCP 系列视频教程(正在施工中)](https://www.bilibili.com/video/BV1zYGozgEHc) + +[锦恢的 mcp 系列博客](https://kirigaya.cn/blog/search?q=mcp) + +[OpenMCP 官方文档]() + +[openmcp-sdk 官方文档]() + +## 频道 + +[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) \ No newline at end of file diff --git a/preview/contributors.md b/preview/contributors.md new file mode 100644 index 0000000..282ae75 --- /dev/null +++ b/preview/contributors.md @@ -0,0 +1,59 @@ +--- +layout: page +--- + + + + + + + + + + \ No newline at end of file diff --git a/preview/join.md b/preview/join.md index f225708..662a36e 100644 --- a/preview/join.md +++ b/preview/join.md @@ -1,62 +1,20 @@ ---- -layout: page ---- +# 参与 OpenMCP 开发 - +## 想要参与开发,如果联系我们? - - - - - - - \ No newline at end of file +如果你也想要参与 OpenMCP 的开发,你可以通过如下的方式联系到我们: + +- 加入 OpenMCP正式级技术组 来和我们直接讨论。 +- 联系锦恢的邮箱 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 的贡献者。 \ No newline at end of file diff --git a/scripts/update-icon.py b/scripts/update-icon.py new file mode 100644 index 0000000..dbef49c --- /dev/null +++ b/scripts/update-icon.py @@ -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') \ No newline at end of file