要把 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.js | 18 或 20 LTS | 运行 TypeScript 编译产物 |
| npm | 随 Node.js 自带 | 安装依赖与执行脚本 |
| TypeScript | 5.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.exampleserver.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 tsxMCP 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 |
| 6 | SDK 版本是否匹配 | 查看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缺失,或数组元素结构不对。