Buckets:
Building Your Own Plugin
Unit 2 produced a text-processor MCP server. In this lesson we package it as a plugin. For Claude Code and Codex that means a manifest plus bundled skills and MCP config; for OpenCode it means a plugin module written in TypeScript, with MCP configuration handled separately; for Pi it means a package declared in package.json, optionally paired with pi-mcp-adapter when you want MCP tools inside Pi.
Plugin Project Structure
Create your plugin directory alongside the server you already built:
mkdir text-processor-plugin
cd text-processor-plugin
# Directory structure
mkdir -p skills/{analyze-text,extract-keywords,check-reading-level}
# Create core files
touch README.md
touch skills/analyze-text/SKILL.md
touch skills/extract-keywords/SKILL.md
touch skills/check-reading-level/SKILL.md
Notice what's different from Unit 2: no server code here. For Claude Code and Codex, the plugin references the MCP server you already built and deployed. OpenCode's native plugin branch below adds behavior in code and can still point at the same server separately if you want both surfaces. The Pi branch below reuses the same skills/ directory inside a Pi package, and can pair those skills with the pi-mcp-adapter setup from Unit 2 when you want MCP tools available in Pi.
Step 1: Write the Skills
This step applies directly to Claude Code, Codex, and Pi, where the bundle can ship skills/. If you're following the OpenCode branch, skip ahead to Step 2: OpenCode plugins are code modules, not skill bundles.
Skills teach the agent when and how to use your MCP tools. Each skill wraps one of the tools from your text-processor server.
Skill 1: Analyze Text
Create skills/analyze-text/SKILL.md:
# Analyze Text
Use the `analyze_text` tool from the text-processor MCP server to compute text statistics.
## When to Use
Use this skill when the user asks about text statistics, word counts, character counts, sentence counts, average word length, or readability metrics.
## How to Use
Call the `analyze_text` tool with the full text as input. The tool returns JSON with:
- `total_characters`, `characters_without_spaces`
- `total_words`, `total_sentences`
- `average_word_length`, `average_sentence_length`
- `unique_words`
## Example
User: "How complex is this paragraph?"
1. Call `analyze_text` with the paragraph
2. Interpret the statistics (high unique word ratio = diverse vocabulary, long average sentence length = complex prose)
3. Summarize findings in plain language
Skill 2: Extract Keywords
Create skills/extract-keywords/SKILL.md:
# Extract Keywords
Use the `extract_keywords` tool from the text-processor MCP server to find the most important words in text.
## When to Use
Use this skill when the user asks for keywords, key terms, topic extraction, or content summarization.
## How to Use
Call `extract_keywords` with the text and an optional `count` parameter (default: 5). The tool returns JSON with a `keywords` array, each containing `word` and `frequency`.
## Example
User: "What are the main topics in this article?"
1. Call `extract_keywords` with count=10
2. Group related keywords into themes
3. Present themes with supporting keyword frequencies
Skill 3: Check Reading Level
Create skills/check-reading-level/SKILL.md:
# Check Reading Level
Use the `check_reading_level` tool from the text-processor MCP server to estimate text difficulty.
## When to Use
Use this skill when the user asks about reading level, text difficulty, grade level, or audience appropriateness.
## How to Use
Call `check_reading_level` with the text. The tool returns JSON with:
- `grade_level` (numeric Flesch-Kincaid grade)
- `reading_level` (Elementary School, Middle School, High School, or College/Academic)
## Example
User: "Is this documentation appropriate for beginners?"
1. Call `check_reading_level` with the text
2. Compare the grade level to the target audience
3. Suggest simplifications if the level is too high
Step 2: Create the Plugin Entry Point
Claude Code and Codex use manifests to declare what the plugin provides. OpenCode uses a plugin module instead. Pi uses package.json plus optional extensions/. The entry point differs by platform.
Create the Claude Code manifest directory:
mkdir -p .claude-plugin
Create .claude-plugin/plugin.json:
{
"name": "text-processor-plugin",
"version": "1.0.0",
"description": "Text analysis skills powered by the text-processor MCP server",
"author": {
"name": "Your Name"
}
}
Create .mcp.json to point at your server. If you're using the local server from Unit 2:
{
"mcpServers": {
"text-processor": {
"command": "python",
"args": ["../text-processor-mcp/server.py"]
}
}
}
Or if you deployed to Hugging Face Spaces:
{
"mcpServers": {
"text-processor": {
"url": "https://YOUR-USERNAME-text-processor-mcp.hf.space/gradio_api/mcp/"
}
}
}
Create the Codex manifest directory:
mkdir -p .codex-plugin
Create .codex-plugin/plugin.json:
{
"name": "text-processor-plugin",
"version": "1.0.0",
"description": "Text analysis skills powered by the text-processor MCP server",
"skills": "./skills/",
"mcpServers": "./.mcp.json",
"interface": {
"displayName": "Text Processor"
}
}
Create .mcp.json to point at your server. For the local server:
{
"mcpServers": {
"text-processor": {
"command": "python",
"args": ["../text-processor-mcp/server.py"]
}
}
}
Or for the Spaces deployment:
{
"mcpServers": {
"text-processor": {
"url": "https://YOUR-USERNAME-text-processor-mcp.hf.space/gradio_api/mcp/"
}
}
}
OpenCode's native plugin layer is code-based, so this is the one branch in the course where the documented plugin surface is not Python. We'll build a minimal local plugin module, then note where skills or MCP config would live if you also want those.
Create the project plugin directory:
mkdir -p .opencode/plugins
Create .opencode/plugins/text-processor-plugin.ts:
import type { Plugin } from "@opencode-ai/plugin"
export const TextProcessorPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
await client.app.log({
body: {
service: "text-processor-plugin",
level: "info",
message: "Text Processor plugin initialized",
},
})
return {
"tool.execute.before": async (input) => {
if (input.tool === "read") {
await client.app.log({
body: {
service: "text-processor-plugin",
level: "info",
message: "Consider a text-analysis workflow after loading long documents.",
},
})
}
},
}
}
This example is intentionally minimal and documented: OpenCode local plugins are startup-loaded JS/TS modules that return hooks. If you want to add custom tools instead of hooks, use the tool() helper from @opencode-ai/plugin.
If you want to install the same plugin from npm instead of a local file, add it to opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@your-org/text-processor-plugin"]
}
If you also want the Unit 2 Python MCP server in OpenCode, configure it separately in opencode.json under the mcp key. That MCP configuration sits beside the plugin system; it is not the plugin itself.
Pi packages use package.json as the manifest. Reuse the same skills/ directory and declare it there:
mkdir -p extensions
Create package.json:
{
"name": "text-processor-plugin",
"version": "1.0.0",
"keywords": ["pi-package"],
"pi": {
"skills": ["./skills"],
"extensions": ["./extensions"]
}
}
Create extensions/text-processor.ts:
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
export default function (pi: ExtensionAPI) {
pi.registerCommand("text-processor-status", {
description: "Confirm the text-processor package is loaded",
handler: async (_args, ctx) => {
ctx.ui.notify(
"text-processor skills loaded. Pair this package with pi-mcp-adapter if you also want MCP tools.",
);
},
});
}
Pi loads the skills/ directory directly from the package. If you want the Unit 2 MCP server available as tools inside Pi, pair this package with the pi-mcp-adapter setup from Unit 2.
Step 3: Create the Human-Facing README
For all three platforms, keep human documentation in README.md. Claude Code and Codex use plugin.json for install metadata; OpenCode loads the plugin module directly, so the README is for developers rather than the runtime.
Create README.md at the plugin root:
# Text Processor Plugin
Plugin examples for text-analysis workflows built from the Unit 2 server and the Unit 3 skills.
## Skills
- **Analyze Text** — Word counts, sentence stats, vocabulary diversity
- **Extract Keywords** — Find the most frequent meaningful terms
- **Check Reading Level** — Flesch-Kincaid grade level estimation
## Claude Code and Codex
The manifest-first versions bundle:
- skills in `skills/`
- plugin metadata in `.claude-plugin/plugin.json` or `.codex-plugin/plugin.json`
- optional MCP config in `.mcp.json`
## OpenCode
The OpenCode version is a local or npm-loaded JS/TS plugin module under `.opencode/plugins/` or the `plugin` array in `opencode.json`.
## Related MCP Server
If you also connect the `text-processor` MCP server, it provides:
- `analyze_text` — Compute text statistics
- `extract_keywords` — Extract frequent terms
- `check_reading_level` — Estimate reading difficulty
- `reverse_text` — Reverse a string
## Setup
The plugin can use your local server or the deployed Spaces version:
**Local:** Ensure `text-processor-mcp/server.py` is available and the MCP runtime is installed.
**Remote:** Update the `.mcp.json` or `opencode.json` URL to your deployed Space:
`https://YOUR-USERNAME-text-processor-mcp.hf.space/gradio_api/mcp/`
Step 4: Test Your Plugin
To test a local plugin, expose it through a local marketplace file. Create marketplace.json next to your plugin directory:
{
"name": "local-example-plugins",
"owner": { "name": "you" },
"plugins": [
{
"name": "text-processor-plugin",
"source": "./text-processor-plugin",
"description": "Text analysis skills"
}
]
}
Then inside Claude Code:
/plugin marketplace add /absolute/path/to/marketplace.json
/plugin install text-processor-plugin@local-example-plugins
Once installed, test the skills conversationally:
Analyze the reading level of this text: "The mitochondria is the powerhouse of the cell.
It provides energy through oxidative phosphorylation."
After editing plugin files, use /plugin to disable and re-enable the plugin so Claude Code picks up changes.
Copy the plugin into your personal plugin directory:
mkdir -p ~/.codex/plugins
cp -R /absolute/path/to/text-processor-plugin ~/.codex/plugins/text-processor-plugin
Add it to your personal marketplace at ~/.agents/plugins/marketplace.json:
Because source.path is resolved relative to the marketplace file, the path below walks back from ~/.agents/plugins/ to ~/.codex/plugins/ first.
{
"name": "local-example-plugins",
"interface": {
"displayName": "Local Example Plugins"
},
"plugins": [
{
"name": "text-processor-plugin",
"source": {
"source": "local",
"path": "../../.codex/plugins/text-processor-plugin"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
Test conversationally:
Analyze the complexity of this paragraph for me
Open the plugin browser to verify the plugin is available:
/plugins
Install the plugin from that marketplace, then start a new thread and test it. If you change the marketplace file or update the local plugin folder, restart Codex before testing again.
Place the local plugin file in .opencode/plugins/, then start OpenCode:
opencode
Because local plugins load at startup, the plugin is active for the session. Test with any workflow that reads a long document:
Summarize this document and note any text-analysis follow-up that would help.
If you also configured the Unit 2 MCP server separately in opencode.json, verify it:
opencode mcp list
If you edit the plugin file or the plugin list in opencode.json, restart OpenCode to reload startup plugins.
Install the package from the local path:
pi install /absolute/path/to/text-processor-plugin -l
pi install npm:pi-mcp-adapter
Add the Unit 2 server to .mcp.json so the package's skills have tools to call:
{
"mcpServers": {
"text-processor": {
"url": "https://YOUR-USERNAME-text-processor-mcp.hf.space/gradio_api/mcp/"
}
}
}
Start Pi:
pi
Test conversationally:
Analyze the complexity of this paragraph for me
Use /reload after editing the package files or .mcp.json so Pi refreshes skills, extensions, and adapter state in the current session.
Complete Plugin Structure
For Claude Code and Codex (assuming you use both), the final manifest-first plugin directory looks like:
text-processor-plugin/
├── .claude-plugin/
│ └── plugin.json # Claude Code manifest
├── .codex-plugin/
│ └── plugin.json # Codex manifest
├── .mcp.json # Shared MCP config for both
├── README.md # Documentation
└── skills/
├── analyze-text/SKILL.md
├── extract-keywords/SKILL.md
└── check-reading-level/SKILL.md
For OpenCode, the plugin surface is separate:
.opencode/
├── plugins/
│ └── text-processor-plugin.ts
└── package.json # optional
Notice there's no server code in the manifest-first plugin. The MCP server lives separately — either running locally from Unit 2's text-processor-mcp/ directory or deployed on Hugging Face Spaces. Claude Code and Codex point at that server through .mcp.json. OpenCode can point at it separately in opencode.json, but that MCP config is adjacent to the plugin rather than inside it.
This separation is a key design principle: skills describe how to use tools, MCP servers provide the tools, and plugins package reusable agent behavior in the way each platform expects.
Best Practices
When building plugins, write skill descriptions that explain when to use a tool (not just what it does), reference existing MCP servers instead of duplicating code, keep only plugin.json inside the manifest directory, never hardcode API keys (use environment variables), version semantically (1.0.0, 1.1.0, 2.0.0), and test locally before publishing. For OpenCode, keep the plugin module small and focused, then add npm dependencies only when the hook or tool really needs them.
Key Takeaways
Claude Code and Codex plugins are manifests that bundle skills and optional MCP or app config. OpenCode plugins are JS/TS modules; skills and MCP servers live beside them. In both worlds the MCP server from Unit 2 stays where it is — the plugin just references or complements it.
Next, a quiz, then installing and using published plugins.
Xet Storage Details
- Size:
- 15.7 kB
- Xet hash:
- fdf20667d206ab790c8db571de8b48b8d0e30ff129145a4994ea73b0978310a1
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.