☰
从零实现 Cosmos.so MCP 服务器:让 AI Agent 轻松检索你的收藏与灵感
2026/9/25 7:24:52 网站建设 项目流程

要把 Cosmos.so 里收藏的书签、图片和灵感片段直接交给 AI Agent 检索,常规做法是在中间搭一个基于 Model Context Protocol(MCP)的桥接服务。Unofficial Cosmos.so MCP 正是这样一类社区项目:它把 Cosmos.so 的空间列表、保存条目和内容检索能力封装成 MCP 工具,接入 Claude Desktop、Cline、Dify、Trae 等支持 MCP 的客户端后,AI 就能按照对话指令完成“读取指定空间里的保存”“搜索和某个灵感相关的收藏”“向某个空间追加一条新收藏”这类操作,而不必把数据库或 CSV 导出文件来回搬运。

MCP 要解决的问题,其实是 AI 应用与外部数据源之间的“接口爆炸”。如果没有统一协议,每接入一个知识库就要写一套私有插件。MCP 通过 JSON-RPC 2.0 定义了一套标准的客户端-服务器通信方式,服务器把数据处理能力声明为 tools,客户端按名称和入参调用。站在使用者角度看,配置一个 MCP 服务器和配置一个本地命令行工具很相似:指定命令、参数和环境变量,客户端以 stdio 子进程方式启动并维护通信。

下面以 Unofficial Cosmos.so MCP 的集成思路为例,从零搭建一个可运行的最小实现。先梳理协议和产品边界,再准备环境,然后编写服务端、接入客户端、验证结果,最后整理排查路径和生产实践。代码使用 TypeScript,依赖@modelcontextprotocol/sdk,不依赖特定云厂商。

1. 理解 Unofficial Cosmos.so MCP 的定位

1.1 MCP 是什么,解决什么问题

MCP 全称 Model Context Protocol,中文常写作“模型上下文协议”。它是一个基于 JSON-RPC 2.0 的开放协议,用于在 AI 客户端和外部工具服务器之间传输能力描述与调用结果。和 REST API 不同,MCP 更关注“能力发现”:客户端启动后会读取服务器提供的工具列表、参数模式和说明,再按需调用。因此接入一个新数据源时,不需要修改客户端主程序,只需要新增一个 MCP 服务器配置。

这个设计在 AI 编程、知识库检索、自动化办公场景中非常实用。比如模型需要查询用户收藏的网页,传统做法是让模型通过代码执行环境请求某个 REST API,并且要手工告诉模型 URL、鉴权方式、返回结构。使用 MCP 后,模型通过读取工具描述就能知道“有一个 list_spaces 工具,不需要额外参数”“有一个 search_saves 工具,需要传入 spaceId 和 keyword”,调用结果以结构化文本返回给模型继续理解。整个链路更规范,也更容易测试。

需要注意,MCP 不是用来取代 REST API 的。它更多是“适配层”,把已经存在的服务能力翻译成 AI 客户端能发现、能调用、能理解的结构。Unofficial Cosmos.so MCP 做的事情,就是把 Cosmos.so 的数据能力翻译成 MCP 工具。

1.2 Cosmos.so 为什么适合作为 AI 数据源

Cosmos.so 是一个面向个人的视觉收藏与管理工具,适合保存书签、灵感图片、设计参考和碎片笔记。它的核心数据结构可以粗略抽象成“空间 Space — 集合 Collection — 保存条目 Save”。用户在网页端或浏览器插件中收藏内容后,后续需要反复检索。这类内容多、标签不统一、关键词不唯一的个人知识库,恰恰是 AI 检索最常用的场景:人工按文件夹找很慢,让模型按关键词或语义过滤反而更高效。

要实现这种检索,普通做法是把个人知识库导出成 JSON 或 Markdown,再交给 AI 客户端。但导出一来不及时,二来无法反向写入。Unofficial Cosmos.so MCP 的设计目标是让 AI 客户端实时读取 Cosmos.so 数据,并根据对话意图发起检索或新增操作。也就是说,它不只是“把数据灌给模型”,而是“让模型能按需访问数据服务”。

1.3 官方与非官方集成之间怎么选

很多热门工具会优先推出自己的官方 MCP 服务。如果你使用的产品已经有官方 MCP,应当优先使用,因为这意味着更稳定的鉴权、错误码和服务托管。非官方项目通常来自社区贡献,大致有几种来源:

  • 对官方公开 API 的二次封装;
  • 对网页端私有接口的模拟调用;
  • 对本地导出文件或数据库的直接读取;
  • 官方未提供 API 时,用浏览器自动化抓取页面的折中方案。

“Unofficial”不一定代表质量差,但使用前要确认三件事:鉴权方式是否安全、接口是否可能随时变化、服务条款是否允许脚本访问。如果项目只依赖浏览器 Cookie,建议只在个人电脑调试时使用,不要部署到团队共享服务器。

维度官方 MCP / 官方 API社区非官方集成
稳定性接口有版本保障字段可能随前端改版变化
鉴权通常提供短期 token可能依赖 Cookie 或私有 token
使用责任官方承担可用性维护使用者自行关注条款与安全
迭代速度跟随官方计划社区可快速适配新功能
适配成本只需要按文档接入需要自行抓接口、维护映射

2. 搭建前的环境准备

2.1 需要准备的工具链

要用 TypeScript 实现一个 MCP 服务器,准备一台能运行 Node.js 的电脑即可。具体工具和用途如下表:

工具建议版本作用
Node.js18 或 20 LTS运行 TypeScript 编译产物
npm随 Node.js 自带安装依赖与执行脚本
TypeScript5.x提供类型检查
tsx最新稳定版开发时直接运行 TS 文件
MCP 客户端Claude Desktop / Cline / Dify / Trae 任一验证服务能否被识别和调用

安装 Node.js 后,先在终端确认版本:

node -v npm -v

如果两条命令都能正常输出版本号,环境就满足要求。客户端选一个就好,不需要全部安装。不同客户端的配置入口可能不同,但核心都是修改一个 JSON 配置,声明mcpServers列表。

2.2 项目结构规划

建议按单包结构组织项目,避免一开始就拆 monorepo。下面这个结构足够支撑最小实现:

cosmos-mcp/ ├─ src/ │ ├─ server.ts # MCP 服务器入口,注册工具 │ ├─ client.ts # Cosmos.so 数据接口的 HTTP 封装 │ └─ local-store.ts # 本地 JSON 模式,离线演示用 ├─ data/ │ └─ saves.json # 本地模拟数据 ├─ package.json ├─ tsconfig.json └─ .env.example

server.ts只负责 MCP 协议相关的逻辑;client.ts封装请求,包括鉴权、错误处理、响应解析;local-store.ts在拿不到真实接口时先跑通链路。这样分层之后,后面切换数据源不用改协议层代码。

2.3 凭证和接口地址怎么处理

MCP 服务器运行在客户端子进程里,配置文件中可以直接写环境变量。不推荐在代码里硬编码 token,原因有三个:token 会在 git 历史中残留;不同机器调试时难以替换;MCP 配置需要分享给团队时容易泄露。建议在.env.example里声明变量名,再在mcpServers的env字段中注入:

COSMOS_BASE_URL=https://api.example.com/v1 COSMOS_API_TOKEN=your_short_lived_token COSMOS_DATA_MODE=local

这里COSMOS_BASE_URL是占位值。如果项目使用的是官方公开 API,以官方文档为准;如果项目基于网络抓包得到的私有接口,要先把接口路径、请求头和响应结构观察清楚,再填入client.ts。

注意:不要在生产日志中输出完整 Cookie 或私有 token。调试信息里出现 Authorization 头时,应先打码再截图。

3. 实现最小可运行的 MCP 服务器

3.1 初始化项目并安装依赖

创建目录后执行:

mkdir cosmos-mcp && cd cosmos-mcp npm init -y npm install @modelcontextprotocol/sdk zod npm install -D typescript @types/node tsx

MCP SDK 提供McpServer类和 stdio 传输实现;zod用来声明工具入参 schema。安装完成后,在package.json中确认或补充"type": "module",否则 NodeNext 模式下的 ES Module 语法会报错:

{ "name": "cosmos-mcp", "type": "module", "scripts": { "dev": "tsx src/server.ts", "build": "tsc", "start": "node dist/server.js" } }

再写tsconfig.json:

{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "dist", "rootDir": "src", "strict": true, "esModuleInterop": true, "skipLibCheck": true }, "include": ["src/**/*"] }

这里的关键是module和moduleResolution都使用NodeNext,然后 import 本地文件时必须带.js后缀。很多新手第一次跑 MCP 示例失败,都是因为没有加后缀,导致 TS 编译后找不到模块。

3.2 编写 HTTP 客户端封装

client.ts的作用是把 Cosmos.so 数据服务封装成几个返回 Promise 的函数。这样 MCP 工具层只关注参数和返回文本,不关心请求细节。

export interface Space { id: string; name: string; } export interface SaveItem { id: string; title: string; url?: string; notes?: string; } const BASE_URL = process.env.COSMOS_BASE_URL || ""; const TOKEN = process.env.COSMOS_API_TOKEN || ""; async function request<T>(path: string, init: RequestInit = {}): Promise<T> { const res = await fetch(`${BASE_URL}${path}`, { ...init, headers: { "Content-Type": "application/json", ...(TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {}), ...(init.headers || {}), }, }); if (!res.ok) { const text = await res.text(); throw new Error(`API ${res.status}: ${text.slice(0, 200)}`); } const raw = await res.text(); return raw ? (JSON.parse(raw) as T) : ({} as T); } export function listSpaces() { return request<{ spaces: Space[] }>("/spaces"); } export function searchSaves(spaceId: string, keyword: string) { return request<{ saves: SaveItem[] }>(`/spaces/${spaceId}/saves?q=${encodeURIComponent(keyword)}`); } export function createSave(input: { spaceId: string; title: string; url?: string; notes?: string; }) { const { spaceId, ...body } = input; return request<{ save: SaveItem }>(`/spaces/${spaceId}/saves`, { method: "POST", body: JSON.stringify(body), }); }

这个代码块中的接口路径是示例。真实项目中,你可能要改为/v2/spaces、GraphQL 批量查询或其他路径。关键是保持request返回 JSON,由工具层决定如何把结果转成文本。

3.3 注册工具并启动服务器

server.ts是核心文件。McpServer注册工具时,工具名建议使用小写字母和下划线组合,描述要写清楚用途,因为模型会根据描述决定何时调用。下面示例注册三个工具:list_spaces、search_saves、create_save。

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { listSpaces, searchSaves, createSave } from "./client.js"; const server = new McpServer({ name: "cosmos-mcp", version: "0.1.0", }); server.registerTool( "list_spaces", { description: "列出当前 Cosmos.so 账号下的所有空间" }, async () => { const data = await listSpaces(); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } ); server.registerTool( "search_saves", { description: "在指定空间内搜索保存条目,支持关键词过滤", inputSchema: { spaceId: z.string().describe("空间 ID"), keyword: z.string().optional().describe("搜索关键词"), }, }, async ({ spaceId, keyword }) => { const data = await searchSaves(spaceId, keyword || ""); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } ); server.registerTool( "create_save", { description: "向指定空间新增一条保存条目", inputSchema: { spaceId: z.string().describe("目标空间 ID"), title: z.string().describe("条目标题"), url: z.string().optional().describe("网页链接"), notes: z.string().optional().describe("备注"), }, }, async ({ spaceId, title, url, notes }) => { const data = await createSave({ spaceId, title, url, notes }); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } ); const transport = new StdioServerTransport(); await server.connect(transport);

这里有两个关键点。第一,MCP 工具返回值必须包含content数组,数组元素是 MCP 协议定义的内容块,常用的是type为text的对象。第二,每个工具的inputSchema使用zod声明,MCP SDK 会在客户端调用时自动校验参数,不需要在函数里再手写校验。如果传入参数类型不对,客户端会收到校验错误而不是业务错误。

3.4 没有真实接口时先用本地 JSON 跑通

社区项目如果还没确定接口版本,可以先写一个本地数据源验证 MCP 链路。新建data/saves.json:

{ "spaces": [ { "id": "space-1", "name": "设计灵感" }, { "id": "space-2", "name": "工程资料" } ], "saves": [ { "id": "save-1", "spaceId": "space-1", "title": "Design System 配色参考", "url": "https://example.com/design-system" }, { "id": "save-2", "spaceId": "space-2", "title": "Rust 异步编程笔记", "url": "https://example.com/rust-async" } ] }

然后在local-store.ts中读取:

import { readFileSync } from "node:fs"; export interface SaveItem { id: string; title: string; url?: string; notes?: string; } export function loadLocalData() { const raw = readFileSync(new URL("../data/saves.json", import.meta.url), "utf-8"); return JSON.parse(raw) as { spaces: Array<{ id: string; name: string }>; saves: SaveItem[]; }; }

接着在client.ts顶部加一个模式判断:

const isLocal = process.env.COSMOS_DATA_MODE === "local";

listSpaces、searchSaves、createSave内部先判断isLocal,本地模式直接返回模拟数据;网络模式才发起真实请求。这样在 MCP 客户端配置里把COSMOS_DATA_MODE设为local,就能在离线环境验证“客户端能识别工具、能传参、能返回结果”。去掉该变量后恢复网络模式。

4. 将 MCP 服务器接入客户端

4.1 标准配置结构

大多数支持 MCP 的客户端都使用同一套mcpServers配置格式:

{ "mcpServers": { "cosmos-mcp": { "command": "npx", "args": ["tsx", "src/server.ts"], "env": { "COSMOS_DATA_MODE": "local", "COSMOS_BASE_URL": "https://api.example.com/v1", "COSMOS_API_TOKEN": "your_token_here" } } } }

如果是在 Claude Desktop 中配置,通常把这段内容合并到claude_desktop_config.json的mcpServers里;如果是在 Cline、Roo Code 这类 VSCode 插件中,则在 MCP 设置面板添加服务器;如果是在 Dify 或 Trae 中,同样有对应 MCP 服务入口。协议层面它们是一致的,差别只是配置位置和是否支持远程地址。

4.2 Windows 系统上怎么创建 MCP

Windows 上最常见的坑是环境变量 PATH 不一致。客户端从图形界面启动时,可能读不到你在终端里配置的 npm 全局路径,导致command为npx时提示“无法识别”。

一种稳定做法是把command改为cmd,通过/c执行命令:

{ "mcpServers": { "cosmos-mcp": { "command": "cmd", "args": ["/c", "npx", "tsx", "C:\\Users\\you\\cosmos-mcp\\src\\server.ts"], "env": { "COSMOS_DATA_MODE": "local" } } } }

如果项目路径包含空格,建议把整个路径放在双引号里。Windows 的 JSON 文件反斜杠需要转义,写C:\\Users\\you\\...。

4.3 验证配置是否生效

配置完成后,重启 MCP 客户端,并在 MCP 面板中查看服务器状态。正常情况下应显示已连接,并列出list_spaces、search_saves、create_save三个工具。如果显示错误,优先看客户端日志,其中通常包含 stderr 输出。

直接在聊天框测试也可以。输入“看看我有哪些空间”,若能返回本地 JSON 中的空间列表,说明配置正确。

注意:如果修改了服务端代码,需要重启客户端或重新加载 MCP 服务器。大多数客户端不会热加载外部 MCP 进程。

5. 运行验证与调试

5.1 命令行验证服务能否启动

在不启动客户端的情况下,可以直接验证 stdio 服务器是否能正常响应:

echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | npx tsx src/server.ts

如果服务器正常,输出会包含 JSON-RPC 响应,里面带有tools数组。不过由于 SDK 运行时可能会先输出日志,实际输出可能带额外内容,因此这条命令主要用于快速确认进程不会立即崩溃。更可靠的验证是打开 MCP 客户端查看工具列表。

5.2 对话式验证

按前面配置发起对话,观察三种情况:

  • “列出 cosmos 里的空间。”预期返回空间名称列表。
  • “在空间 space-1 中搜索包含 设计 的保存。”预期返回 JSON 数组。
  • “往空间 space-1 新增一个保存,标题为 React 官方文档,链接为 https://react.dev 。”如果使用本地模式,数据不会真正落盘,只返回模拟结果;网络模式下才会写入真实账户。

本地 JSON 模式下,create_save返回的是模拟成功,不会修改data/saves.json。这是为了避免开发时误操作真实账号。如果要验证写入,再启动网络模式,并使用测试空间。

5.3 返回结果裁剪,减少 token 消耗

如果工具返回了完整数据,但模型仍然理解不了,常见原因是返回 JSON 太大或字段结构不直观。建议在工具内部做一次精简,只返回关键字段:

const summary = data.saves.map((s) => ({ id: s.id, title: s.title, url: s.url || "", })); return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }], };

这样做能减少 token 消耗,也让模型更容易识别关键信息。返回 JSON 并不是必须的,也可以返回 Markdown 列表,只要文本里信息完整即可。

5.4 通过日志观察工具调用

MCP 服务器通常以 stdio 子进程方式运行,调试时可以把工具调用信息写到 stderr。在自定义工具函数内部加一行:

console.error(`[cosmos-mcp] search_saves called, spaceId=${spaceId}, keyword=${keyword}`);

注意必须使用console.error,不要用console.log。因为 stdout 是 MCP 协议通道,普通文本输出会污染 JSON-RPC 消息,导致客户端解析失败。

6. 常见问题排查

6.1 排查顺序

按照“配置 -> 进程 -> 参数 -> 网络 -> 日志”的顺序排查,比直接改代码更高效。

步骤检查点命令或位置
1客户端配置路径是否正确mcpServers中command和args
2服务能否单独启动直接运行npx tsx src/server.ts
3依赖是否安装完整检查node_modules和package-lock.json
4环境变量是否注入客户端配置文件里的env字段
5网络和鉴权接口返回 401 / 403 / 429
6SDK 版本是否匹配查看package.json中@modelcontextprotocol/sdk版本

6.2 三个高频错误

错误一:服务器显示 error,客户端日志提示找不到 npx 或找不到源文件。

原因通常是 PATH 不完整,或源码路径写错。解决方式是先确认npx可执行,再把command改为npx的绝对路径,args也使用绝对路径。Windows 下优先使用cmd /c方案。

错误二:调用工具后返回 401。

现象是模型能列出工具,但真正执行时报 HTTP 401。原因可能是环境变量没有生效,或 token 已过期。检查客户端配置文件里env字段是否完整,不要依赖 shell 里的export。私有接口还要检查请求头名称,某些私有接口要求 Cookie 而不是 Authorization。

错误三:客户端提示 content 不符合协议格式。

原因通常是返回对象里content缺失,或数组元素结构不对。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询