Plugin Development Best Practices
Guidelines and recommendations for building high-quality CortexPrism plugins.
General Principles
1. Single Responsibility
Each plugin should do one thing well. If you find yourself adding unrelated capabilities, split them into separate plugins.
// Good: focused plugin
export default definePlugin({
name: "csv-parser",
capabilities: { parseCSV, validateCSV, transformCSV },
});
// Bad: mixed concerns
export default definePlugin({
name: "data-utils",
capabilities: { parseCSV, sendEmail, resizeImage, queryDatabase },
});
2. Fail Gracefully
Always handle errors and provide meaningful messages:
// Good
async function fetchData(url: string, ctx: CapabilityContext) {
if (!url.startsWith("https://")) {
throw new PluginError("Only HTTPS URLs are supported");
}
try {
return await ctx.sandbox.fetch(url);
} catch (err) {
throw new PluginError(`Failed to fetch ${url}: ${err.message}`);
}
}
3. Validate Inputs
Use Zod schemas or explicit validation for all capability inputs:
const Input = z.object({
email: z.string().email(),
template: z.string().min(1).max(10000),
variables: z.record(z.string()).optional(),
});
4. Respect Timeouts
Capabilities have a default timeout of 30 seconds. Support cancellation:
async function processLargeFile(path: string, ctx: CapabilityContext) {
const stream = await ctx.sandbox.readFileStream(path);
for await (const chunk of stream) {
if (ctx.abortSignal.aborted) {
throw new TimeoutError("Processing cancelled");
}
// process chunk
}
}
5. Declare Minimal Permissions
Only request the permissions you actually use:
{
"permissions": {
"network": ["fetch:https://api.example.com"],
"filesystem": ["read:/tmp/data"]
}
}
ESM-Specific
Use TypeScript
TypeScript provides better IDE support and catches errors at build time:
interface SearchResult {
title: string;
url: string;
snippet: string;
}
export default definePlugin({
name: "web-search",
capabilities: {
async search(query: string): Promise<SearchResult[]> {
// Type-safe implementation
},
},
});
Bundle for Distribution
Bundle your plugin into a single file for reliable distribution:
deno bundle mod.ts dist/plugin.js
Or for npm-published plugins:
npx tsc && npx esbuild dist/index.js --bundle --format=esm --outfile=dist/bundle.js
Avoid Global State
Each capability call may run in a fresh context. Use factory functions for stateful plugins:
export default async function createPlugin() {
const connection = await createConnectionPool();
return {
name: "db-connector",
capabilities: {
async query(sql: string) {
return connection.execute(sql);
},
},
};
}
MCP-Specific
Handle Process Lifecycle
MCP servers should handle graceful shutdown:
process.on("SIGTERM", async () => {
await cleanup();
process.exit(0);
});
process.on("SIGINT", async () => {
await cleanup();
process.exit(0);
});
Minimize Startup Time
Keep MCP server initialization fast. Defer expensive setup to the first tool call:
let client: DatabaseClient | null = null;
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (!client) {
client = await createClient(); // Deferred initialization
}
// handle tool call
});
Stream Large Results
For large outputs, use streaming responses:
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const stream = await getLargeResultStream();
return {
content: [{ type: "text", text: stream.readAll() }],
};
});
WASM-Specific
Optimize for Size
[profile.release]
opt-level = "s" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Better optimization
strip = true # Remove debug symbols
Use Simple Types
WASM ABI works best with primitive types. Use JSON for complex data:
// Good: use JSON for structured data
#[derive(Deserialize)]
struct Input {
values: Vec<f64>,
threshold: f64,
}
// Avoid: complex ABI types
fn process(values: *const f64, count: u32, threshold: f64) -> u32 { }
Test with wasmtime
Always test your WASM plugin outside of CortexPrism first:
wasmtime run --dir=. plugin.wasm
Testing Guidelines
Unit Tests
import { testPlugin } from "@cortexprism/plugin-sdk/testing";
const harness = await testPlugin(myPlugin);
Deno.test("my capability works", async () => {
const result = await harness.call("myCap", { input: "test" });
assertEquals(result, { expected: "output" });
});
Deno.test("my capability rejects invalid input", async () => {
await assertRejects(
() => harness.call("myCap", { input: "" }),
PluginError,
);
});
Integration Tests
Test your plugin in a real CortexPrism session:
cortex plugin install ./my-plugin
cortex chat --plugin my-plugin --no-stream <<< "Use my-plugin to do something"
Documentation
Every plugin should include:
- README.md: Usage instructions, examples, configuration options
- manifest.json: Complete metadata for marketplace display
- Inline comments: For complex logic in capability implementations
- Example inputs: In capability descriptions for LLM context
What to Avoid
- Hardcoded secrets: Use the vault or environment variables
- Synchronous blocking: All capabilities must be async
- Side effects without cleanup: Always clean up in
onDeactivate - Dependency on specific LLM providers: Keep capabilities provider-agnostic
- Overly broad permissions: Request minimum required access