Skip to content

Latest commit

 

History

History
162 lines (127 loc) · 6.59 KB

File metadata and controls

162 lines (127 loc) · 6.59 KB

MCP Plugin System

Overview

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.

Architecture

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}/…

Key Components

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

Plugin Manifest

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. Use https:// or file:// in production.

Plugin MCP Entry Module

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;

Plugin SDK (@paca-ai/plugin-sdk-mcp)

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 Naming

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_]*.

Loading Behaviour

  1. The MCP server fetches GET /api/v1/plugins using the configured PACA_API_KEY.
  2. Plugins where enabled: false are skipped.
  3. Plugins without manifest.mcp.remoteEntryUrl are skipped (they may still have frontend or backend extensions).
  4. For each qualifying plugin, the server calls import(remoteEntryUrl) and validates the default export.
  5. 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.
  6. Core tools are always available regardless of plugin load failures.

Security Considerations

  • 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 PluginAPIClient authenticates 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 remoteEntryUrl at 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, all remoteEntryUrl values should use https://.

Error Handling

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

Comparison with the Frontend Plugin System

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