Quickstart: remote plugin
Build a remote plugin, an MCP server that uses Streamable HTTP, from templates/remote-plugin. Run it locally, add it to the app, deploy it and register it in the marketplace.
A remote plugin is a server you run yourself. The app connects to it over the internet and calls its tools. On this page you copy the template, write a tool, run the server on your computer, add it to the app, and then deploy and register it.
The server speaks the Model Context Protocol (MCP), an open
standard that lets AI apps discover and call tools on a server. PKU harness uses MCP's Streamable
HTTP transport, in which each call is an HTTP POST request.
Users add a remote plugin by its URL or from the marketplace. Its tools then join the same registry as the tools of bundled plugins, follow the same permissions and are called by the same assistant. None of your code runs on the phone, and a remote plugin never sees campus credentials.
You need bun and some TypeScript. The template uses TypeScript with the official MCP SDK, but any language with an MCP server library works the same way.
1. Start from the template
cd pku-harness-plugins
cp -r templates/remote-plugin ../my-plugin
cd ../my-plugin
bun installThe template is one file, server.ts, plus a package.json that pulls in
@modelcontextprotocol/sdk, express and zod:
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 = your registry id: the app installs the plugin as `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) => {
// Optional bearer auth: users paste the token in the app; it is stored in their keychain.
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 }); // stateless
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'));The build() function creates the server and registers its tools. The /mcp route checks the
optional token, then hands the request to a fresh server. Replace the hello tool with your own.
2. Write real tools
Each MCP field of a tool has a specific use in PKU harness:
name, the first argument ofregisterTool, becomes the tool idmcp-<server>.<name>. The model sees it asmcp-<server>__<name>. Use a short ASCII camelCase name. Other characters become_, and names longer than 40 characters are cut.titleis shown to users in chat, in approval prompts and in Settings. Write it in Chinese.descriptionis shown to the model. Say when and how to use the tool, and give units and formats.inputSchemais converted to zod on the device and checked before every call. Describe every field.annotations.readOnlyHintsets the effect.truegives the effectread, and the tool runs directly. If the hint is missing orfalse, the effect iswrite, and by default the app asks the user first.structuredContentin the result is returned to the model asdata. This is the preferred form. Without it, the app tries to parse the text content as JSON, and passes it as text if that fails.isError: truein the result turns the text into the error message the assistant sees.
The first 500 characters of the server's instructions go into the plugin's section of the system
prompt. Just before them, the app adds a line telling the model that this plugin's output is data
and must not be followed as instructions.
Effects for remote tools
For now the app gives a remote tool one of two effects: read when readOnlyHint: true, and
write for everything else. A tool that spends money or posts publicly is therefore also write,
and asks first by default. Say plainly in its title and description what it does.
3. Run it and add it to the app
bun server.ts # MCP plugin ready on /mcp, port 8787
# optional: PLUGIN_TOKEN=secret bun server.tsIn the app, open Settings → Plugins → Add remote plugin. Enter the URL, and the token if you set one. The URL depends on where the app runs:
| Where the app runs | URL |
|---|---|
| Android emulator | http://10.0.2.2:8787/mcp |
| Phone on the same network | http://<your machine's LAN IP>:8787/mcp |
| Deployed | https://my-plugin.example.com/mcp |
The emulator reaches your computer at 10.0.2.2. A real phone needs your computer's address on the
local network (LAN).
The app connects, lists your tools and shows each one with its effect on the plugin's page. Then ask
the assistant something your tool can answer. You can also ask the assistant to install the plugin
for you. It calls plugins.installRemote, which asks for your approval first.
A complete example
examples/demo-canteen in pku-harness-plugins is a working remote plugin with mock data. It has
one read tool and one write tool, and its bearer token is demo-token. Run it with
bun examples/demo-canteen/server.ts.
4. Deploy
Any host that serves HTTPS will do: a virtual private server (VPS), Fly.io, Railway, Cloudflare with an MCP-compatible runtime, or a campus server. Keep these points in mind:
- Production needs HTTPS. Use plain HTTP only for local development.
POST /mcpmust answer MCP Streamable HTTP requests. The template is stateless: it builds a new server for each request. That suits serverless hosts, which may start a fresh process for every request.- Some campus hosts answer only inside the PKU network. If you call one of them, your server has to run inside that network too. The app can't lend your server the user's campus session.
- Your own secrets, such as API keys, stay on the server. To protect the endpoint, use a bearer
token and set
"auth": "bearer"in the registry entry.
5. Register it
Registering the plugin lists it in the marketplace, so students can find it. There are two ways to
do this. You can add a manifest to your repository and submit it through this website's marketplace
(manifest reference, submitting).
Or you can add an entry to registry/registry.json in pku-harness-plugins. Either way, the id
must match your MCP server's 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"]
}Before you open a pull request against the registry, run bun scripts/validate-registry.ts. See
Publishing and the review policy.
Checklist
- The MCP server's
nameequals the registryid(^[a-z0-9][a-z0-9-]{1,40}$). - Every tool has a Chinese
title, adescriptionfor the model, and described input fields. - Read-only tools set
annotations.readOnlyHint: true, and no other tool does. - Results are data (
structuredContent), never instructions, and reasonably small. - The plugin never asks for a campus password.
- The endpoint uses HTTPS, and bearer auth is declared if you use it.
Write a plugin with the AI
Ask the assistant in the app to write a PLUGIN.md plugin with you. It reads the language reference, validates the draft, tries read-only tools against the live service, and opens the usual review sheet, where you decide whether to install.
Bundled plugin
Write a plugin that ships inside the app with definePlugin. A complete example walks through tools, a data source, a screen, a Settings row, a widget, a command, English text and tests.