Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
- name: Setup Node.js with npm registry
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "24"
registry-url: "https://registry.npmjs.org"
scope: "@paca-ai"

Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,20 @@ interface PluginMCPEntry {
args: Record<string, unknown>,
context: PluginMCPContext,
): Promise<PluginToolResult>;

// Optional: contribute additional text to the response of ANY core tool
// call (not just tools this plugin owns). Switch on toolId to decide
// what, if anything, to add; return null/undefined for the rest.
getToolContext?(
toolId: string,
args: Record<string, unknown>,
context: PluginMCPContext,
): Promise<string | null | undefined>;
}
```

See [mcp-plugin-system.md](../paca/docs/plugins/mcp-plugin-system.md#contributing-to-any-core-tools-response-gettoolcontext) for the full `getToolContext` contract and examples.

### `PluginMCPContext`

Runtime context injected by the host into every `handleToolCall` call.
Expand Down
50 changes: 50 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,49 @@ export interface PluginMCPEntry {
args: Record<string, unknown>,
context: PluginMCPContext,
): Promise<PluginToolResult>;

/**
* Optional: contribute additional text to the response of **any** core
* Paca tool call (not just tools this plugin owns) — e.g. attach a
* "linked branches" section to `get_task`, or a note to `list_sprints`.
*
* Called once per loaded plugin after every successful core tool call.
* Switch on `toolId` to decide what — if anything — to contribute; return
* `null` (or `undefined`) for tools/args the plugin has nothing to add
* for, so the host omits the section instead of rendering empty
* boilerplate on every call.
*
* Errors are caught and logged by the host; a failing plugin cannot
* blank out the rest of the response, so this does not need its own
* try/catch purely for that purpose.
*
* @param toolId - The core tool's name, e.g. `"get_task"`,
* `"list_tasks"`, `"get_project"`. See `ALL_TOOLS.md` in `apps/mcp`
* for the full list of core tool names and their argument shapes.
* @param args - The raw arguments the AI client passed for this call
* (same shape the tool itself receives — not necessarily containing
* every ID your plugin needs; e.g. `get_task_by_number` has no
* `taskId`, only `taskNumber`).
*
* @example
* ```ts
* async getToolContext(toolId, args, context) {
* if (toolId !== "get_task") return null;
* const { projectId, taskId } = args as { projectId: string; taskId: string };
* const api = new PluginAPIClient(context);
* const items = await api.pluginGet<Item[]>(
* `projects/${projectId}/tasks/${taskId}/items`,
* );
* if (items.length === 0) return null;
* return `## My Plugin\n\n${items.map((i) => `- ${i.title}`).join("\n")}`;
* }
* ```
*/
getToolContext?(
toolId: string,
args: Record<string, unknown>,
context: PluginMCPContext,
): Promise<string | null | undefined>;
}

// ── Context ───────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -124,6 +167,13 @@ export interface InstalledPlugin {
mcp?: {
/** URL to the plugin's MCP entry module (Node.js-compatible ESM). */
remoteEntryUrl: string;
/**
* Core tool IDs (e.g. `"get_task"`) this plugin's `getToolContext`
* can contribute to. The host only invokes `getToolContext` for
* plugins that declare the tool here — implementing the method
* without declaring it here means it is never called.
*/
toolContextHooks?: string[];
};
};
}
Loading