PKU harness

快速上手:远程插件

基于 templates/remote-plugin 编写远程插件,也就是一个使用 Streamable HTTP 的 MCP 服务器。在本地运行它,添加到 App,再部署上线并登记到插件市场。

远程插件(remote plugin)是你自己运行的服务器,App 通过网络连接它、调用它的工具。本页带你复制模板、编写一个工具、在电脑上运行服务器、把它添加到 App,最后部署上线并登记到插件市场。

服务器使用 MCP(Model Context Protocol,模型上下文协议)。MCP 是一项开放标准,让 AI 应用能够发现并调用服务器上的工具。PKU harness 使用 MCP 的 Streamable HTTP 传输方式,每次调用都是一个 HTTP POST 请求。

用户可以通过 URL 或插件市场添加远程插件。添加之后,它的工具和内置插件的工具进入同一个注册表,遵守同一套权限,由同一个助手调用。你的代码不会在手机上运行,远程插件也永远拿不到校园账号密码。

你需要 bun 和一些 TypeScript 基础。模板使用 TypeScript 和官方 MCP SDK,不过任何有 MCP 服务端库的语言,做法都一样。

1. 从模板开始

cd pku-harness-plugins
cp -r templates/remote-plugin ../my-plugin
cd ../my-plugin
bun install

模板只有一个文件 server.ts,外加一个 package.json,依赖 @modelcontextprotocol/sdk、express 和 zod:

server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';
import { z } from 'zod';

function build() {
  // name 就是你在注册表里的 id:App 会以 `mcp-<name>` 的名字安装这个插件。
  const server = new McpServer(
    { name: 'my-plugin', version: '0.1.0', title: '我的插件' },
    { instructions: 'What this plugin is for, in one sentence.' },
  );
  server.registerTool(
    'hello',
    {
      title: '打个招呼',
      description: 'Return a greeting. Replace with a real capability.',
      inputSchema: { name: z.string().describe('who to greet') },
      annotations: { readOnlyHint: true },
    },
    async ({ name }) => ({
      content: [{ type: 'text', text: `你好,${name}` }],
      structuredContent: { greeting: `你好,${name}` },
    }),
  );
  return server;
}

const app = express();
app.use(express.json());
app.post('/mcp', async (req, res) => {
  // 可选的 bearer 认证:用户在 App 里粘贴 token,它保存在用户设备的钥匙串里。
  if (process.env.PLUGIN_TOKEN && req.headers.authorization !== `Bearer ${process.env.PLUGIN_TOKEN}`) {
    res.status(401).json({ jsonrpc: '2.0', error: { code: -32001, message: 'unauthorized' }, id: null });
    return;
  }
  const server = build();
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); // 无状态
  res.on('close', () => {
    transport.close();
    server.close();
  });
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});
app.listen(Number(process.env.PORT ?? 8787), () => console.log('MCP plugin ready on /mcp'));

build() 函数创建服务器并注册工具。/mcp 路由先检查可选的 token,再把请求交给一个新建的服务器处理。把 hello 工具换成你自己的工具即可。

2. 编写真正的工具

工具的每个 MCP 字段在 PKU harness 里都有特定的用途:

  • name,即 registerTool 的第一个参数,会成为工具 id mcp-<server>.<name>,模型看到的名字是 mcp-<server>__<name>。请用简短的 ASCII 小驼峰命名。其他字符会被替换成 _,超过 40 个字符的部分会被截掉。
  • title 显示给用户,出现在聊天、授权确认和设置页里。用中文写。
  • description 给模型看。写清楚什么时候用、怎么用,以及单位和格式。
  • inputSchema 会在设备上转换成 zod,每次调用前都会校验。每个字段都要写说明。
  • annotations.readOnlyHint 决定工具的效果。设为 true 时效果是 read,工具直接运行;不写或设为 false 时效果是 write,App 默认会先询问用户。
  • 结果中的 structuredContent 会作为 data 返回给模型,这是推荐的写法。没有它时,App 会尝试把文本内容解析成 JSON,解析失败就原样作为文本传递。
  • 结果中的 isError: true 会让文本内容成为助手看到的错误信息。

服务器 instructions 的前 500 个字符会写进系统提示词中属于这个插件的那一段。App 会在它前面加一句话,提醒模型:这个插件的输出只是数据,不能当作指令执行。

远程工具的效果

目前 App 只给远程工具两种效果(effect):设置了 readOnlyHint: true 的是 read,其余都是 write。所以会花钱或公开发帖的工具也被归为 write,默认先询问。请在它的 title 和 description 里写清楚它做什么。

3. 运行并添加到 App

bun server.ts                       # 在 8787 端口的 /mcp 上提供服务
# 可选:PLUGIN_TOKEN=secret bun server.ts

在 App 里打开 设置 → 插件 → 添加远程插件,填入 URL;如果设置了 token,也一并填上。URL 取决于 App 运行在哪里:

App 运行在URL
Android 模拟器http://10.0.2.2:8787/mcp
同一网络下的手机http://<你电脑的局域网 IP>:8787/mcp
已部署https://my-plugin.example.com/mcp

模拟器通过 10.0.2.2 访问你的电脑;真机则需要你的电脑在局域网里的地址。

App 会连接服务器,列出你的工具,并在插件页面上显示每个工具的效果。然后问助手一个你的工具能回答的问题。你也可以请助手替你安装插件:它会调用 plugins.installRemote,安装前会先请你确认。

完整示例

pku-harness-plugins 里的 examples/demo-canteen 是一个能运行的远程插件,使用模拟数据。它有一个读取工具和一个修改工具,bearer token 是 demo-token。用 bun examples/demo-canteen/server.ts 运行它。

4. 部署

任何能提供 HTTPS 的地方都可以:虚拟专用服务器(VPS)、Fly.io、Railway、带有 MCP 兼容运行时的 Cloudflare,或者校内服务器。请注意以下几点:

  • 生产环境必须使用 HTTPS,纯 HTTP 只用于本地开发。
  • POST /mcp 必须能处理 MCP Streamable HTTP 请求。模板是无状态的:每个请求都新建一个服务器。这很适合 serverless 平台,因为这类平台可能为每个请求启动一个新进程。
  • 有些校内服务只在北大校园网内可以访问。如果你要调用这类服务,你的服务器也必须运行在校园网里。App 无法把用户的校园登录状态借给你的服务器。
  • 你自己的密钥(比如 API key)留在服务器上。如果要保护接口,请使用 bearer token,并在注册表条目里写上 "auth": "bearer"。

5. 登记到插件市场

登记之后,插件会出现在插件市场里,同学们就能找到它。登记有两种方式。一种是在你的仓库里加一份清单,再通过本站的插件市场提交(见清单参考和提交插件)。另一种是在 pku-harness-plugins 的 registry/registry.json 里加一条记录。无论哪种方式,id 都必须和 MCP 服务器的 name 一致:

{
  "id": "my-plugin",
  "name": "我的插件",
  "description": "一句话说明它做什么。",
  "author": "your-github-name",
  "source": "https://github.com/you/my-plugin",
  "kind": "remote",
  "endpoint": "https://my-plugin.example.com/mcp",
  "auth": "none",
  "requires": ["network"],
  "scopes": ["example.com"],
  "tags": ["canteen"]
}

向注册表提交 pull request 之前,请先运行 bun scripts/validate-registry.ts。另见发布和审核规则。

检查清单

  • MCP 服务器的 name 和注册表 id 相同(^[a-z0-9][a-z0-9-]{1,40}$)。
  • 每个工具都有中文 title、写给模型的 description,每个输入字段都有说明。
  • 只读工具设置了 annotations.readOnlyHint: true,其他工具都没有设置。
  • 返回的是数据(structuredContent),不含指令,体积适中。
  • 插件从不索要校园账号密码。
  • 接口使用 HTTPS;如果用了 bearer 认证,已在注册表中声明。

本页目录