The Paca MCP server supports plugin-contributed tools. Each installed Paca plugin can ship a small Node.js-compatible ESM module that declares MCP tool definitions and handles tool calls. When the MCP server starts, it fetches the list of enabled plugins from the API and dynamically loads any plugin that declares an mcp.remoteEntryUrl in its manifest.
From the AI client's perspective, plugin tools appear alongside core Paca tools in a single flat list — there is no visible distinction.
MCP Client (Claude, Copilot, Cursor, …)
│
│ stdio
▼
Paca MCP Server (apps/mcp)
│
├── Startup: GET /api/v1/plugins
│ │
│ └── For each plugin with mcp.remoteEntryUrl:
│ import(remoteEntryUrl) ← dynamic ESM import
│ validate PluginMCPEntry
│ collect tools + register handler
│
├── ListTools → [core tools] + [plugin tools]
│
└── CallTool → route to plugin registry OR core handlers
│
└── plugin handler calls Paca API
/api/v1/plugins/{pluginId}/…
| Component | Location | Purpose |
|---|---|---|
plugin-loader.ts |
apps/mcp/src/ |
Fetches plugin list, imports modules, builds PluginRegistry |
PluginRegistry |
apps/mcp/src/plugin-loader.ts |
Holds loaded plugins, merges tools, routes calls |
server.ts |
apps/mcp/src/ |
Async server factory; integrates plugin registry |
@paca-ai/plugin-sdk-mcp |
plugin-sdk-mcp/ |
SDK for plugin developers |
Add an mcp section to your plugin.json:
{
"id": "com.example.my-plugin",
"displayName": "My Plugin",
"version": "1.0.0",
"mcp": {
"remoteEntryUrl": "https://cdn.example.com/my-plugin/1.0.0/mcp.js"
}
}remoteEntryUrl points to the plugin's MCP entry module — a Node.js-compatible ESM bundle built from your plugin source. The MCP server dynamically imports it via import(url).
Local development:
http://URLs are supported. The server fetches the source over HTTP and re-evaluates it internally. Usehttps://orfile://in production.
The module must export a PluginMCPEntry object as its default export:
import type { PluginMCPEntry } from "@paca-ai/plugin-sdk-mcp";
import { PluginAPIClient, textResult, errorResult } from "@paca-ai/plugin-sdk-mcp";
const entry: PluginMCPEntry = {
tools: [
{
name: "checklist_list_items",
description: "List checklist items attached to a task.",
inputSchema: {
type: "object",
properties: {
project_id: { type: "string", description: "Project ID" },
task_id: { type: "string", description: "Task ID" },
},
required: ["project_id", "task_id"],
},
},
],
async handleToolCall(name, args, context) {
const api = new PluginAPIClient(context);
const { project_id, task_id } = args as { project_id: string; task_id: string };
try {
if (name === "checklist_list_items") {
const items = await api.pluginGet(`projects/${project_id}/tasks/${task_id}/items`);
return textResult(JSON.stringify(items, null, 2));
}
return errorResult(`Unknown tool: ${name}`);
} catch (err) {
return errorResult(err instanceof Error ? err.message : String(err));
}
},
};
export default entry;The @paca-ai/plugin-sdk-mcp package provides:
PluginMCPEntry— interface your default export must implement.PluginMCPContext— runtime context injected by the host (pluginId,baseURL,apiKey).PluginAPIClient— scoped HTTP client for calling your plugin's backend routes.textResult(text)/errorResult(message)— helpers for building tool results.Tool— re-exported MCP tool definition type.
See the SDK README and sdk-reference.md for full API documentation.
Tool names must be unique across all enabled plugins. Use a short prefix derived from your plugin ID:
| Plugin ID | Prefix | Example tool name |
|---|---|---|
com.paca.checklist |
checklist_ |
checklist_list_items |
com.paca.bdd |
bdd_ |
bdd_list_scenarios |
com.example.timetracking |
timetracking_ |
timetracking_log_hours |
Tool names must match [a-z][a-z0-9_]*.
- The MCP server fetches
GET /api/v1/pluginsusing the configuredPACA_API_KEY. - Plugins where
enabled: falseare skipped. - Plugins without
manifest.mcp.remoteEntryUrlare skipped (they may still have frontend or backend extensions). - For each qualifying plugin, the server calls
import(remoteEntryUrl)and validates the default export. - If a plugin fails to load (network error, invalid module, etc.), a warning is logged to stderr and the server continues with the remaining plugins.
- Core tools are always available regardless of plugin load failures.
- Plugin MCP modules run in the same Node.js process as the MCP server with no sandboxing (v1). Only install plugins from trusted sources.
- The
PluginAPIClientauthenticates using the MCP server's API key. Plugin access is scoped by Paca's existing authorization model (routes under/api/v1/plugins/{pluginId}/). - The server fetches
remoteEntryUrlat startup — not at every tool call — so the module is cached for the server's lifetime. http://URLs are permitted for local development only. In production, allremoteEntryUrlvalues should usehttps://.
| Scenario | Behaviour |
|---|---|
| API unreachable at startup | Warning logged; server starts with no plugins |
| Plugin module fetch fails | Warning logged; that plugin's tools are unavailable |
| Plugin module has invalid shape | Warning logged; that plugin's tools are unavailable |
Plugin handleToolCall throws |
Error returned to AI client as an isError: true result |
| Aspect | Frontend (Module Federation) | MCP (Dynamic Import) |
|---|---|---|
| Load time | Lazy — on first navigation | Eager — at server startup |
| Runtime | Browser (ES modules) | Node.js 18+ |
| SDK | @paca-ai/plugin-sdk-react |
@paca-ai/plugin-sdk-mcp |
| Entry field | frontend.remoteEntryUrl |
mcp.remoteEntryUrl |
| Sandboxing | Browser origin isolation | Same process (v1) |
| Lifecycle | Loaded per browser session | Loaded once per server process |