Guidelines and recommendations for building high-quality CortexPrism plugins.
Each plugin should do one thing well. If you find yourself adding unrelated capabilities, split them into separate plugins.
// Good — focused plugin
export const tools = [parseCSV, validateCSV, transformCSV];
// Bad — mixed concerns
export const tools = [parseCSV, sendEmail, resizeImage, queryDatabase];Always handle errors and provide meaningful messages. Never throw from tool execute — return failure results instead.
execute: async (args, ctx) => {
if (!args.url || typeof args.url !== 'string') {
return {
toolName: 'fetch_data',
success: false,
output: '',
error: 'Missing required parameter: url',
durationMs: 0,
};
}
try {
const res = await fetch(args.url);
const data = await res.text();
return {
toolName: 'fetch_data',
success: true,
output: data,
durationMs: 0,
};
} catch (err) {
return {
toolName: 'fetch_data',
success: false,
output: '',
error: `Failed to fetch ${args.url}: ${err.message}`,
durationMs: 0,
};
}
}Validate all tool parameters at the top of execute:
execute: async (args, ctx) => {
// Required param validation
if (!args.email || typeof args.email !== 'string' || !args.email.includes('@')) {
return { toolName: 'send_email', success: false, error: 'Invalid email', durationMs: 0, output: '' };
}
if (args.template && typeof args.template !== 'string') {
return { toolName: 'send_email', success: false, error: 'template must be a string', durationMs: 0, output: '' };
}
// ... proceed
};Tool execution can be interrupted. Track time and handle cancellation:
execute: async (args, ctx) => {
const start = Date.now();
// Check for timeout/cancellation
// Track durationMs accurately
return {
toolName: 'my_tool',
success: true,
output: result,
durationMs: Date.now() - start,
};
};Only request the capabilities your plugin actually uses. Declare them in the manifest and per-tool:
{
"capabilities": ["tools", "network:fetch"],
"tools": [
{
"name": "search",
"params": [],
"description": "Search the web"
}
]
}const searchTool: Tool = {
definition: {
name: 'search',
description: 'Search the web',
params: [],
capabilities: ['network:fetch'], // per-tool permissions — subset of manifest capabilities
},
execute: async (args, ctx) => { /* ... */ },
};TypeScript provides better IDE support and catches errors at build time. All plugin code should be strictly typed.
Each capability call may run in a fresh context. Use factory functions or the PluginContext for state:
// Good — use PluginContext.state
export const onLoad = async (ctx: PluginContext) => {
const connection = await createConnectionPool();
await ctx.state.set('connectionPool', JSON.stringify({ id: connection.id }));
};
// Good — lazy initialization in tools
let pool: ConnectionPool | null = null;
const dbTool: Tool = {
definition: { name: 'db_query', /* ... */ },
execute: async (args, ctx) => {
if (!pool) {
pool = await createConnectionPool();
}
return pool.query(args.sql);
},
};Always clean up resources in onUnload:
let interval: number | null = null;
export const onLoad = async (ctx: PluginContext) => {
interval = setInterval(() => ctx.logger.info('heartbeat'), 60000);
};
export const onUnload = async (ctx: PluginContext) => {
if (interval) {
clearInterval(interval);
interval = null;
}
};MCP servers should handle graceful shutdown:
process.on('SIGTERM', async () => {
await cleanup();
process.exit(0);
});
process.on('SIGINT', async () => {
await cleanup();
process.exit(0);
});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
});For large outputs, use streaming responses where possible.
# Rust Cargo.toml
[profile.release]
opt-level = "s" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Better optimization
strip = true # Remove debug symbolsWASM ABI works best with primitive types. Use JSON for complex data.
Always test your WASM plugin outside of CortexPrism first:
wasmtime run --dir=. plugin.wasm# Install and enable
cortex plugins install ./my-plugin
cortex plugins enable my-plugin
# Verify it loads
cortex plugins list
# Use in a chat session
cortex chatCheck the plugin status and error messages:
cortex plugins list # overview
cortex plugins permissions my-plugin # permission detailsPlugin data is stored at ~/.cortex/data/plugins/<name>/ — inspect state.json for persisted state.
Every plugin should include:
- README.md — Usage instructions, examples, configuration options
- manifest.json — Complete metadata (all required fields, settings schema)
- Inline type annotations — For complex data structures in tool params
- Clear tool descriptions — These are consumed by LLMs to understand when to use your tool
- Hardcoded secrets — Use
ctx.configor environment variables; never commit credentials - Synchronous blocking — All tool
executefunctions must be async - Side effects without cleanup — Always clean up in
onUnload - Overly broad permissions — Request minimum required capabilities
- Crashing the host — Never throw from tool execution; return failure results instead
- Silent failures — Always log errors via
ctx.logger.error()and return meaningful error messages - Undocumented config keys — Every config key your plugin reads should be declared in
ui.settings