PKU harness
Concepts

Plugins

Plugins come in three kinds. Plugin documents (PLUGIN.md) are interpreted by the app and installed by link. Bundled TypeScript plugins ship inside the app. Remote plugins are MCP servers added by URL. All three share one registry, one permission system and one agent loop.

In PKU harness, each feature that connects to a campus service is provided by a plugin. There are three kinds of plugin. The most common is a plugin document: a folder with a PLUGIN.md file that the app reads and follows. The other two kinds are TypeScript modules built into the app, and remote plugins, which run on a server and connect through the Model Context Protocol (MCP).

The agent, the permission system and the marketplace handle all three kinds in the same way. Built-in and community plugins use the same permission system.

The three kinds of plugin

Plugin documents

A plugin document is a folder. It contains a YAML manifest, instructions for the agent, and tools written as YAML steps. If the plugin has widgets, they are described as generative-UI JSON.

A plugin document runs on the phone, where the app interprets it. No plugin code runs at any point.

It is published as a link, either to a GitHub folder or to any PLUGIN.md URL. To update the plugin, you git push. The official plugin documents are also bundled with the app, and they too update from their links.

A plugin document can reach the campus services it declares (using the user's session) and public websites, and it can use host abilities. It can provide widgets, screens, Settings rows and commands.

Bundled TypeScript plugins

A bundled plugin is a TypeScript module built on @pku-harness/sdk with definePlugin. It runs on the phone inside the app, and it ships with a new app release or a signed over-the-air (OTA) update.

A bundled plugin can use host services, native UI and PKU harness Cloud. Native UI here means screens, widgets, Settings rows, commands and tabs.

Remote plugins

A remote plugin is an MCP server that communicates over Streamable HTTP. You can write it in any language, and it runs on any HTTPS server you control.

To publish a remote plugin, you deploy your server. Users then add its URL, or install it from the marketplace. A remote plugin can only provide tools, and its results are passed to the model as data.

The three kinds compared

Plugin documentBundled pluginRemote plugin
Runs onThe phone, interpreted by the appThe phone, inside the appYour HTTPS server
Published asA linkPart of the appA deployed server
Sees campus credentialsNoNoNo
Tool ids<plugin id>.<tool><plugin id>.<tool>mcp-<server name>.<tool>
Example tool idclassrooms.freewallet.cardBalancemcp-demo-canteen.menu

None of the three kinds ever sees campus credentials. For a plugin document, the harness signs the campus requests itself. A bundled plugin only receives responses through services.campus.request. A remote plugin has no campus session at all.

Trust

Plugin documents are either official or community. Official documents are bundled with the app or come from the pkuinfo/pku-harness-plugins repository. Tools from community documents ask you before any write, money or public effect, and their results are treated as untrusted.

Bundled plugins are all reviewed in the plugins repository.

Remote plugins are third-party code. By default, any tool that changes data asks you first. Every result from a remote plugin taints the current turn of the conversation. The security model explains what this means.

Why most plugins are documents

App stores forbid apps from downloading executable code. A plugin document is data, not code, so community plugins can run on the phone as documents, or remotely over MCP.

The official campus plugins classrooms, campus-events, grades, canteen, shuttle and notices are all plugin documents. TypeScript is used only for the harness's own features, such as UI, the dashboard, the scheduler, the schedule, to-dos, memory and model providers. It is also used for services that the plugin language can't express yet.

How to choose a kind

Is it campus or public requests plus extraction (with the user's campus session if needed)?
├─ yes → plugin document (PLUGIN.md). Start from the quickstart, or ask the AI in the app.
└─ no  → Does it need general code or a server of your own?
         ├─ yes → remote plugin. Start from templates/remote-plugin.
         └─ no, it needs the device or native UI the language lacks
              → bundled TypeScript plugin (open an issue first), or propose a host ability.

Most campus services only need to send requests and extract data from the responses. A plugin document is enough for these. Start from the quickstart, or ask the AI in the app to write one for you.

If you need general code or your own server, write a remote plugin, starting from the templates/remote-plugin template. Write a bundled TypeScript plugin only when you need a device feature or native UI that the plugin language doesn't support yet, and open an issue first. You can also propose a new host ability so that plugin documents can do the job.

Lifecycle in the app

From installation to use, a plugin goes through five stages.

  1. Registered. Bundled plugins and bundled documents are registered when the app starts. A document installed by link is registered after you accept its review sheet, and its files and a content hash are stored on the phone. A remote plugin is registered when you install it: the app connects to the server, lists its tools and caches its manifest.
  2. Available. A plugin is available when the phone meets the capabilities it lists in requires. Unavailable plugins are hidden from the agent, and Settings explains why.
  3. Enabled. Plugins are on by default. You can turn off any plugin that is not a core plugin. Core plugins are marked core: true, for example generative UI and memory, and can't be turned off.
  4. Called. Every call follows the same path. The app validates the input with zod, checks the mode set for the tool's effect, asks for your approval if needed, runs the tool and returns the result.
  5. Updated. This stage applies only to plugin documents. The app fetches the plugin's link again at most once a day. If the new content declares the same access, it takes effect at once. If it declares wider access, it waits until you confirm. See Declarative plugins → Updates.

Remote plugins in detail

Adding. You, or the agent with your approval, add an MCP Streamable HTTP URL and, optionally, a bearer token. The token is stored in the keystore.

Plugin id. A remote plugin's id is mcp- followed by a slug of the MCP server's name, which comes from the initialize response. Use your registry id as the server name, so that the marketplace can match the two.

Tools. Each MCP tool becomes one tool in the registry, and its input schema is converted from JSON Schema to zod. A tool marked annotations.readOnlyHint: true becomes a read tool. Every other tool becomes a write tool.

Default modes. For remote plugins, read tools run automatically, and write, money and public tools ask you first.

Results. Results are wrapped as { source, data }. The data is taken from structuredContent, or from the text parsed as JSON. The system prompt has a section about the plugin that tells the model to treat these results as data and never to follow them as instructions.

Tool names. MCP tool names are cleaned to contain only [a-zA-Z0-9_-]. Any other character becomes _, and names are cut to 40 characters. Keep them short and in ASCII camelCase.

Merging and renaming plugins

The PluginDef.legacy field lets a plugin take over the ids of older plugins. For example, the wallet plugin replaced campus-card, netfee and utilities.

With this field set, the host maps the old plugin ids, tool ids and widget keys to the new ones, and moves your existing settings over once. See SDK → plugin.

Further reading

On this page