# Plugin SDK reference

> Typed APIs for @paca-ai/plugin-sdk-react, plugin-sdk-go host bridge, and @paca-ai/plugin-sdk-mcp; extension points and scoped HTTP clients.

- Repository: Paca-AI/paca
- GitHub: https://github.com/Paca-AI/paca
- Human docs: https://grok-wiki.com/public/docs/paca-ai-paca-f238b2ab3d25
- Complete Markdown: https://grok-wiki.com/public/docs/paca-ai-paca-f238b2ab3d25/llms-full.txt

## Source Files

- `docs/plugins/sdk-reference.md`
- `docs/plugins/backend-plugin-system.md`
- `docs/plugins/frontend-plugin-system.md`
- `docs/plugins/mcp-plugin-system.md`
- `docs/plugins/developer-guide.md`
- `services/api/internal/transport/http/dto/plugin_dto.go`

---

---
title: "Plugin SDK reference"
description: "Typed APIs for @paca-ai/plugin-sdk-react, plugin-sdk-go host bridge, and @paca-ai/plugin-sdk-mcp; extension points and scoped HTTP clients."
---

Paca plugin authors integrate through three published SDK packages — `@paca-ai/plugin-sdk-react` for Module Federation UI, `github.com/Paca-AI/plugin-sdk-go` for WASM backend handlers, and `@paca-ai/plugin-sdk-mcp` for MCP tool modules. Each SDK wraps scoped HTTP access to `/api/v1/plugins/{pluginId}/…` (and, where applicable, core `/api/v1/` routes) so plugins never construct unscoped clients against the host.

## Package map

| Package | Runtime | Repository | Install |
|---|---|---|---|
| `@paca-ai/plugin-sdk-react` | Browser (Module Federation remote) | [github.com/Paca-AI/plugin-sdk-react](https://github.com/Paca-AI/plugin-sdk-react) | `bun add @paca-ai/plugin-sdk-react` |
| `github.com/Paca-AI/plugin-sdk-go` | WASM (`GOOS=wasip1 GOARCH=wasm`) in `services/api` | [github.com/Paca-AI/plugin-sdk-go](https://github.com/Paca-AI/plugin-sdk-go) | `go get github.com/Paca-AI/plugin-sdk-go` |
| `@paca-ai/plugin-sdk-mcp` | Node.js ESM in `apps/mcp` | [github.com/Paca-AI/plugin-sdk-mcp](https://github.com/Paca-AI/plugin-sdk-mcp) | `bun add @paca-ai/plugin-sdk-mcp` |

<Note>
All three SDKs live in separate repositories. The Paca monorepo hosts the runtime that loads plugin artifacts and enforces permissions; SDKs are the typed contract plugin code compiles against.
</Note>

A working end-to-end reference is [github.com/Paca-AI/paca-plugin-example](https://github.com/Paca-AI/paca-plugin-example).

```mermaid
flowchart LR
  subgraph browser["Browser — apps/web host"]
    EP["ExtensionPoint / PluginSlot"]
    RE["@paca-ai/plugin-sdk-react"]
    EP --> RE
  end

  subgraph api["services/api"]
    RT["plugin.Runtime — wazero"]
    GO["plugin-sdk-go"]
    RT --> GO
    RTR["/api/v1/plugins/:pluginId/*path"]
  end

  subgraph mcp["apps/mcp"]
    PL["plugin-loader.ts"]
    MCP["@paca-ai/plugin-sdk-mcp"]
    PL --> MCP
  end

  RE -->|"pluginGet / listTasks"| RTR
  MCP -->|"PluginAPIClient"| RTR
  MCP -->|"coreGet"| CORE["/api/v1/…"]
```

## Scoped HTTP paths

Backend routes declared in `plugin.json` under `backend.routes` are proxied at:

```
/api/v1/plugins/{pluginId}/{path}
```

Project-scoped handlers should include `/projects/:projectId/` in the manifest path (for example `/projects/:projectId/tasks/:taskId/items`). The Gin router captures the full sub-path via a wildcard (`/plugins/:pluginId/*path`) and dispatches to the plugin WASM handler after per-route middleware from the manifest.

| Client | Prefix | Auth |
|---|---|---|
| `PluginApiClient` (React) | `/api/v1` + `plugins/{pluginId}/` | Session cookie via host-injected `fetch` (`credentials: "include"`) |
| `PluginAPIClient` (MCP) | `/api/v1/plugins/{pluginId}/` for plugin routes; `/api/v1/` for `coreGet` | `X-API-Key` from `PluginMCPContext.apiKey` |
| Go route handlers | N/A — in-process WASM | Caller identity from `req.Caller()` (JWT claims validated by host) |

<Warning>
For project-scoped plugin routes, include `projects/{projectId}/` in relative paths passed to `pluginGet`, `pluginPost`, and MCP `PluginAPIClient` helpers. Omitting the project segment produces 404s against the mounted namespace.
</Warning>

---

## `@paca-ai/plugin-sdk-react`

TypeScript/React SDK for frontend extension point components loaded via Vite Module Federation. Mark `@paca-ai/plugin-sdk-react` as a **shared singleton** in the plugin's `vite.config.ts` so the host and remote use the same SDK instance.

### `PluginSDK`

The host injects a `PluginSDK` object on every extension point component:

```ts
interface PluginSDK {
  api: PluginApiClient;
  ui: PluginUI;
  meta: PluginMeta;
}
```

Extension point prop interfaces extend `BaseExtensionProps`, which flattens `api`, `ui`, and `meta` to top-level props alongside point-specific fields.

### `PluginApiClient`

<ParamField body="baseUrl" type="string" required>
API root, for example `https://app.example.com/api/v1`. Set by the host.
</ParamField>

<ParamField body="projectId" type="string" required>
Current project UUID. Injected by the host for project-scoped calls.
</ParamField>

<ParamField body="fetch" type="function" required>
Host-provided `fetch` wrapper that attaches session credentials. Plugins must not construct their own client.
</ParamField>

| Method | Returns | Scope |
|---|---|---|
| `listTasks(filters?)` | `TaskSummary[]` | Current project |
| `getTask(taskId)` | `Task` | Current project |
| `getProject()` | `ProjectSummary` | Current project |
| `listMembers()` | `ProjectMember[]` | Current project |
| `pluginGet<T>(pluginId, path)` | `T` | `/plugins/{pluginId}/{path}` |
| `pluginPost<T>(pluginId, path, body)` | `T` | same |
| `pluginPatch<T>(pluginId, path, body)` | `T` | same |
| `pluginDelete(pluginId, path)` | `void` | same |

<RequestExample>

```ts
// Backend route registered at /tasks/:taskId/items
const items = await api.pluginGet<MyItem[]>(
  meta.pluginId,
  `projects/${projectId}/tasks/${taskId}/items`,
);

const members = await api.listMembers();
const tasks = await api.listTasks({ status_ids: ["done"] });
```

</RequestExample>

### `PluginUI`

| Method | Behavior |
|---|---|
| `toast(opts)` | Host toast: `title`, optional `description`, `variant` (`default` \| `success` \| `destructive`), `duration` |
| `confirm(opts)` | Modal; resolves `true` if confirmed. `title` required. |
| `navigate(path)` | In-app navigation via host router |

### `PluginMeta`

<ResponseField name="pluginId" type="string">
Reverse-DNS identifier, for example `com.paca.checklist`.
</ResponseField>

<ResponseField name="displayName" type="string">
Human-readable plugin name from manifest.
</ResponseField>

<ResponseField name="version" type="string">
Semver from manifest.
</ResponseField>

### Extension point contracts

The host recognizes five extension point IDs. Each exported remote component must match the typed props for its point:

| Extension point ID | Host surface | SDK props interface | Additional props |
|---|---|---|---|
| `task.detail.section` | Task detail drawer/page | `TaskDetailSectionProps` | `taskId`, `projectId` |
| `sidebar.general.section` | Global sidebar | `SidebarGeneralSectionProps` | `isCollapsed` |
| `sidebar.project.section` | Project sidebar | `SidebarProjectSectionProps` | `projectId`, `isCollapsed` |
| `project.settings.tab` | Project settings | `ProjectSettingsTabProps` | `projectId` |
| `view` | Board/view area | `ViewExtensionProps` | `projectId`, `viewConfig?` |

```ts
interface BaseExtensionProps {
  api: PluginApiClient;
  ui: PluginUI;
  meta: PluginMeta;
}

interface TaskDetailSectionProps extends BaseExtensionProps {
  taskId: string;
  projectId: string;
}
```

The host's `<ExtensionPoint>` forwards point-specific `componentProps` (for example `{ projectId, taskId }` on task detail) to every registered remote at that point, sorted by manifest `order` and admin `extension_settings`.

### Shared types

Task and project shapes use **snake_case** field names matching REST JSON:

```ts
interface TaskSummary {
  id: string;
  title: string;
  task_number: number;
  status_id: string | null;
  assignee_id: string | null;
}

interface TaskFilters {
  status_ids?: string[];
  assignee_ids?: string[];
  sprint_id?: string;
  parent_task_id?: string;
  page?: number;
  page_size?: number;
}
```

### React Query integration

`PluginQueryClientProvider` namespaces TanStack Query cache keys under `["plugin", pluginId, …]` so plugin queries cannot collide with the host or sibling plugins.

```ts
import {
  PluginQueryClientProvider,
  usePluginQuery,
  usePluginQueryClient,
} from "@paca-ai/plugin-sdk-react";

function MyComponent({ api, meta, taskId }: TaskDetailSectionProps) {
  const { data, isLoading } = usePluginQuery(
    meta.pluginId,
    ["my-items", taskId],
    () => api.pluginGet(meta.pluginId, `projects/${projectId}/tasks/${taskId}/items`),
  );
}
```

`PluginQueryClientProvider` accepts an optional `queryClient` to reuse the host's `QueryClient` when running inside the federation shell.

---

## `github.com/Paca-AI/plugin-sdk-go`

Go SDK for WASM backend plugins executed by `services/api/internal/platform/plugin.Runtime` (wazero). The SDK hides the linear-memory protobuf host bridge behind idiomatic Go types.

### Build target

<Tabs>
<Tab title="TinyGo (smaller binary)">

```sh
GOOS=wasip1 GOARCH=wasm tinygo build -o plugin.wasm -target wasip1 .
```

</Tab>
<Tab title="Standard Go 1.21+">

```sh
GOOS=wasip1 GOARCH=wasm go build -o plugin.wasm .
```

</Tab>
</Tabs>

Every plugin entry file uses the `wasip1` build tag and calls `plugin.Run` from `init()`:

```go
//go:build wasip1

package main

import plugin "github.com/Paca-AI/plugin-sdk-go"

type myPlugin struct {
    db  *plugin.DB
    kv  *plugin.KV
    log *plugin.Logger
    cfg *plugin.Config
}

func (p *myPlugin) Init(ctx *plugin.Context) error {
    p.db  = ctx.DB()
    p.kv  = ctx.KV()
    p.log = ctx.Log()
    p.cfg = ctx.Config()

    ctx.Route("GET",  "/tasks/:taskId/items", p.listItems)
    ctx.Route("POST", "/tasks/:taskId/items", p.createItem)
    ctx.On("task.deleted", p.onTaskDeleted)
    return nil
}

func (p *myPlugin) Shutdown() {}

func init() { plugin.Run(&myPlugin{}) }
func main() {}
```

### `Plugin` interface

| Lifecycle | SDK surface | Host behavior |
|---|---|---|
| Startup | `Init(ctx *plugin.Context) error` | Host calls after loading WASM; register routes and event handlers here |
| HTTP | `ctx.Route(method, path, handler)` | Routes matched under `/api/v1/plugins/{pluginId}/…` |
| Events | `ctx.On(topic, handler)` | Subscriptions declared in `backend.eventSubscriptions` |
| Shutdown | `Shutdown()` | Called before module unload |

### Request and response helpers

| Symbol | Purpose |
|---|---|
| `req.PathParam(name)` | Route parameter from manifest path |
| `req.Caller()` | `CallerIdentity` with `ProjectID`, user claims |
| `plugin.JSONBody[T](req)` | Deserialize request body |
| `resp.JSON(status, data)` | JSON response |
| `resp.Error(status, message)` | Error response |
| `resp.NoContent()` | 204 response |
| `plugin.JSONPayload[T](evt)` | Parse domain event payload |
| `plugin.DispatchEvent(ctx, topic, payload)` | Fire event in tests |

### Host bridge (Go SDK wrappers)

The SDK maps to host functions registered in `plugin.Runtime`:

| SDK type | Host functions | Constraints |
|---|---|---|
| `*plugin.DB` | `paca.db_query`, `paca.db_exec`, transactions | `Query`/`Exec` run in plugin schema `plugin_data_{pluginId}`; SELECT-only for `db_query` |
| `*plugin.KV` | `paca.storage_get/set/delete` | JSONB key-value per plugin |
| `*plugin.Logger` | `paca.log` | Structured logs tagged with plugin ID |
| `*plugin.Config` | `paca.config_get` | Keys allowlisted in `backend.allowedConfigKeys` |
| Core reads | `paca.tasks_list`, `paca.task_get`, `paca.project_get`, `paca.members_list` | Project scope enforced |
| Outbound HTTP | `paca.fetch` | Domains allowlisted in `backend.allowedOutboundDomains` |
| Events | `paca.event_emit` | Plugin-namespaced topics to Valkey |

Default resource limits per WASM instance: **64 MiB** memory (`MaxMemoryPages: 1024`), **5 s** max call duration.

### Unit testing with `plugintest`

```go
tc := plugintest.NewContext(t)
tc.DB.SeedRows("my_items", []string{"id", "task_id", "title"}, [][]any{{"abc", "task-1", "Test"}})
tc.Config.Set("greeting.prefix", "Hi")

var p myPlugin
p.Init(tc.PluginContext())

res := tc.Call("GET", "/tasks/:taskId/items", plugintest.Request{
    PathParams: map[string]string{"taskId": "task-1"},
    Caller:     plugin.CallerIdentity{ProjectID: "proj-1"},
})
```

| `plugintest` API | Description |
|---|---|
| `NewContext(t)` | Fresh harness; auto cleanup |
| `DB.SeedRows` / `DB.AllRows` | In-memory table seeding and inspection |
| `KV.Set`, `Config.Set` | Pre-seed state |
| `PluginContext()` | `*plugin.Context` for `Init` |
| `Call(method, path, req)` | Dispatch route; returns `*plugin.Response` |

---

## `@paca-ai/plugin-sdk-mcp`

TypeScript SDK for MCP tool modules loaded at `apps/mcp` startup. Declare `mcp.remoteEntryUrl` in `plugin.json`; the MCP server imports the ESM bundle, validates `PluginMCPEntry`, and merges tools into the flat tool list.

### `PluginMCPEntry`

```ts
interface PluginMCPEntry {
  tools: Tool[];
  handleToolCall(
    name: string,
    args: Record<string, unknown>,
    context: PluginMCPContext,
  ): Promise<PluginToolResult>;
}
```

Default-export the entry object from `src/mcp.ts` (or equivalent build output).

### `PluginMCPContext`

<ParamField body="pluginId" type="string" required>
Reverse-DNS plugin name (for example `com.paca.checklist`).
</ParamField>

<ParamField body="baseURL" type="string" required>
Paca API base URL (for example `http://localhost:8080`).
</ParamField>

<ParamField body="apiKey" type="string" required>
API key from MCP server config (`PACA_API_KEY`). Sent as `X-API-Key`.
</ParamField>

Construct one `PluginAPIClient` per tool invocation:

| Method | Prefix |
|---|---|
| `pluginGet/Post/Patch/Delete(path)` | `/api/v1/plugins/{pluginId}/` |
| `coreGet(path)` | `/api/v1/` |

### `PluginToolResult`

```ts
interface PluginToolResult {
  content: ToolResultContent[];
  isError?: boolean;
}

type ToolResultContent =
  | { type: "text"; text: string }
  | { type: "image"; data: string; mimeType: string }
  | { type: "resource"; resource: { uri: string; text?: string; mimeType?: string } };
```

Helpers: `textResult(text)` for success, `errorResult(message)` sets `isError: true`.

### Tool naming

Tool names must be globally unique across all enabled plugins. Use a short prefix derived from the plugin ID:

| Plugin ID | Prefix | Example |
|---|---|---|
| `com.paca.checklist` | `checklist_` | `checklist_list_items` |
| `com.example.my-plugin` | `my_plugin_` | `my_plugin_create_item` |

Pattern: `[a-z][a-z0-9_]*`.

### Loading and errors

<AccordionGroup>
<Accordion title="Startup sequence">

1. `GET /api/v1/plugins` with `X-API-Key`
2. Skip `enabled: false` or missing `mcp.remoteEntryUrl`
3. `import(remoteEntryUrl)` — `https://`, `file://`, or `http://` (localhost/gateway only)
4. Validate default export; register tools in `PluginRegistry`

</Accordion>
<Accordion title="Failure modes">

| Scenario | Behavior |
|---|---|
| API unreachable at startup | Warning logged; MCP starts with core tools only |
| Module fetch/validation fails | Warning logged; that plugin's tools omitted |
| `handleToolCall` throws | `isError: true` text result returned to client |
| Duplicate tool name | Later plugin's tool skipped with warning |

</Accordion>
</AccordionGroup>

<RequestExample>

```ts
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" },
        task_id:    { type: "string" },
      },
      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 {
      const items = await api.pluginGet(
        `projects/${project_id}/tasks/${task_id}/items`,
      );
      return textResult(JSON.stringify(items, null, 2));
    } catch (err) {
      return errorResult(err instanceof Error ? err.message : String(err));
    }
  },
};

export default entry;
```

</RequestExample>

---

## Manifest fields that bind SDKs

| Manifest section | SDK | Key fields |
|---|---|---|
| `frontend` | React | `remoteEntryUrl`, `extensionPoints[].point`, `extensionPoints[].component` |
| `backend` | Go | `routes`, `eventSubscriptions`, `migrations`, `allowedConfigKeys`, `allowedOutboundDomains` |
| `mcp` | MCP | `remoteEntryUrl` |
| `permissions` | Go host bridge | Gates host function groups (`db:write:plugin_data`, `http:register_routes`, `events:emit`, …) |

Route middleware names: `authn`, `optionalAuthn`, `requireFreshPassword`, `requireJWTAuth`, `requirePermissions` (with `scope`, `projectParam`, `permissions`). Omitted `middlewares` applies the default chain: `optionalAuthn` → `requireFreshPassword` → project-scoped `projects.read`.

---

## Quick integration checklist

<Steps>
<Step title="Install the SDK for your surface">

Add `@paca-ai/plugin-sdk-react`, `plugin-sdk-go`, or `@paca-ai/plugin-sdk-mcp` to the plugin project matching the surfaces you ship.

</Step>
<Step title="Match extension point or route contracts">

React components implement the typed props for their `extensionPoints[].point`. Go plugins register paths that align with `backend.routes` and include `/projects/:projectId/` when scoped to a project.

</Step>
<Step title="Use scoped clients only">

Call `api.pluginGet(meta.pluginId, path)` in React, `PluginAPIClient` in MCP, and `ctx.Route` handlers in Go — never call `/api/v1` directly from plugin bundles with hard-coded credentials.

</Step>
<Step title="Verify against a running stack">

```sh
# Backend route
curl -s -b "session=…" \
  "http://localhost:8080/api/v1/plugins/com.example.my-plugin/projects/{pid}/tasks/{tid}/items"

# Plugin list (MCP loader input)
curl -s -H "X-API-Key: …" http://localhost:8080/api/v1/plugins
```

Open a task detail panel to confirm frontend remotes load, and restart the MCP server to pick up new `mcp.remoteEntryUrl` modules.

</Step>
</Steps>

## Related pages

<CardGroup>
<Card title="Plugin system" href="/plugin-system">
WASM sandbox, extension points, MCP tools, capability permissions, and marketplace install lifecycle.
</Card>
<Card title="Build a plugin" href="/build-plugin">
End-to-end authoring: manifest, WASM backend, frontend remotes, migrations, and local install.
</Card>
<Card title="MCP tools reference" href="/mcp-tools-reference">
Core `@paca-ai/paca-mcp` tools plus dynamically loaded plugin tools.
</Card>
<Card title="REST API reference" href="/rest-api">
Versioned `/api/v1` paths, auth envelopes, and resource endpoints plugins may call via scoped clients.
</Card>
</CardGroup>
