4257307063
Co-authored-by: Kandev Agent <agent@kandev.dev>
267 lines
12 KiB
Markdown
267 lines
12 KiB
Markdown
---
|
|
status: draft
|
|
created: 2026-08-11
|
|
owner: Kandev
|
|
---
|
|
|
|
# Plugin-Contributed Agent Tools
|
|
|
|
## Why
|
|
|
|
Plugin authors can extend Kandev's UI and data behavior, but task agents cannot
|
|
invoke plugin-owned operations. Authors currently have to ship and configure a
|
|
separate MCP server, which duplicates plugin installation, lifecycle, trust,
|
|
and task-context handling. Kandev needs one supported way for an installed
|
|
plugin to contribute task-aware tools to agents.
|
|
|
|
Decision: [ADR-2026-08-11-plugin-tools-through-kandev-mcp](../../decisions/2026-08-11-plugin-tools-through-kandev-mcp.md).
|
|
|
|
Implementation plan: [Plugin-Contributed Agent Tools](../../plans/plugin-agent-tools/plan.md).
|
|
|
|
## What
|
|
|
|
- A managed plugin package (`runtime.type: binary`) may declare zero or more
|
|
agent tools in its manifest. Each declaration has a plugin-local name,
|
|
description, input JSON Schema, optional output JSON Schema, supported task
|
|
surfaces, and optional MCP annotations. Legacy remote HTTP plugins cannot
|
|
declare agent tools because they do not implement the managed gRPC runtime.
|
|
- Only active plugins contribute tools. Registered, disabled, errored, and
|
|
uninstalled plugins contribute none.
|
|
- Plugin tools are exposed through the existing task-aware Kandev MCP server.
|
|
Plugins do not run or register a second MCP server.
|
|
- Kandev derives a readable provider-safe MCP name as
|
|
`kandev_<plugin-id-slug>_<local-name>`, where the plugin ID slug replaces
|
|
punctuation with underscores. The manifest does not choose an arbitrary
|
|
global MCP name. If the readable name exceeds the provider limit, Kandev
|
|
truncates the slug and appends a short stable hash suffix. Install
|
|
validation still rejects a final-name collision.
|
|
- A declared tool may target `kanban-task`, `office-task`, or both. Plugin tools
|
|
are not exposed to `configuration` or `external` MCP surfaces in this version.
|
|
- Installing, upgrading, enabling, disabling, degrading, recovering, or
|
|
uninstalling a plugin recomputes the authoritative plugin-tool catalog.
|
|
- Running MCP servers replace their effective tool registry atomically. A
|
|
changed registry emits one `notifications/tools/list_changed` notification;
|
|
the agent process, task session, and MCP server do not restart.
|
|
- A client that does not honor `tools/list_changed` sees the new catalog after
|
|
its next MCP reconnect or task-session restart. Kandev does not terminate a
|
|
healthy agent process solely to force discovery refresh.
|
|
- Tool invocation is a single request to the already supervised plugin process.
|
|
Kandev supplies immutable invocation ID, task ID, session ID, workspace ID,
|
|
and MCP surface context derived from the running session. Those values are not
|
|
accepted from tool arguments.
|
|
- The plugin returns required fallback text, optional structured JSON content,
|
|
and an `is_error` value. Structured content must conform to the declared
|
|
output schema when one is present.
|
|
- Tool calls have a 30-second deadline, propagate cancellation, are never
|
|
automatically retried, and return at most 1 MiB across fallback text and
|
|
structured content.
|
|
- Manifest validation limits each plugin to 16 tools, local names to 32
|
|
characters, descriptions to 1,024 bytes, and each serialized schema to
|
|
64 KiB. Input and output schemas must compile and have an object root.
|
|
- MCP annotations are hints, not authorization. Omitted annotations use
|
|
conservative defaults: `readOnlyHint=false`, `destructiveHint=true`,
|
|
`idempotentHint=false`, and `openWorldHint=true`.
|
|
- Declaring an agent tool grants only discovery and invocation of that RPC. It
|
|
grants no additional Host API capability; existing `api_read`, `api_write`,
|
|
`state`, `secrets`, `agent_invoke`, and `auth` gates remain authoritative.
|
|
- Discovery is not an authorization boundary. Every invocation revalidates the
|
|
active plugin record, exact declared tool, allowed surface, backend-bound
|
|
task/session pair, caller scope, and input schema before calling the plugin.
|
|
- Plugin tool calls are recorded in structured logs with invocation ID, plugin
|
|
ID, local tool name, task ID, session ID, duration, and outcome. Arguments,
|
|
structured output, secret values, and plugin configuration are not logged.
|
|
|
|
## Manifest Contract
|
|
|
|
```yaml
|
|
agent_tools:
|
|
- name: add_tag
|
|
description: Add an existing tag to the current task.
|
|
surfaces: [kanban-task]
|
|
input_schema:
|
|
type: object
|
|
properties:
|
|
tag_id:
|
|
type: string
|
|
required: [tag_id]
|
|
additionalProperties: false
|
|
output_schema:
|
|
type: object
|
|
properties:
|
|
task_id:
|
|
type: string
|
|
tag_id:
|
|
type: string
|
|
required: [task_id, tag_id]
|
|
additionalProperties: false
|
|
annotations:
|
|
read_only_hint: false
|
|
destructive_hint: false
|
|
idempotent_hint: true
|
|
open_world_hint: false
|
|
```
|
|
|
|
`name` must match `^[a-z0-9][a-z0-9_]{0,31}$`. `surfaces` must contain one
|
|
or both supported task surfaces without duplicates. `input_schema` is required;
|
|
`output_schema` and `annotations` are optional. Unknown fields continue to
|
|
follow the manifest's existing compatibility rules.
|
|
|
|
An install or upgrade fails before changing the active installation when its
|
|
declarations are invalid, when two declarations produce the same exposed name,
|
|
or when a declaration conflicts with a tool name reserved by another installed
|
|
plugin version. A failed package change leaves the previous installed version
|
|
and its tools unchanged.
|
|
|
|
## Plugin SDK Contract
|
|
|
|
Plugins that contribute declared tools implement the optional `AgentToolPlugin`
|
|
interface:
|
|
|
|
```go
|
|
type AgentToolPlugin interface {
|
|
InvokeAgentTool(context.Context, *AgentToolRequest) (*AgentToolResult, error)
|
|
}
|
|
```
|
|
|
|
The author-facing types are:
|
|
|
|
```go
|
|
type AgentToolRequest struct {
|
|
InvocationID string
|
|
Name string
|
|
Arguments map[string]any
|
|
Context AgentToolContext
|
|
}
|
|
|
|
type AgentToolContext struct {
|
|
TaskID string
|
|
SessionID string
|
|
WorkspaceID string
|
|
Surface string
|
|
}
|
|
|
|
type AgentToolResult struct {
|
|
Text string
|
|
StructuredContent map[string]any
|
|
IsError bool
|
|
}
|
|
```
|
|
|
|
The base `Plugin` interface remains unchanged. The SDK's gRPC adapter checks
|
|
whether the implementation satisfies `AgentToolPlugin` and returns gRPC
|
|
`Unimplemented` otherwise. `UnimplementedPlugin.InvokeAgentTool` also returns
|
|
`Unimplemented`, so both embedding and non-embedding existing implementations
|
|
remain source-compatible. Kandev calls the method only for a declaration present
|
|
in the active manifest.
|
|
|
|
## Runtime Catalog
|
|
|
|
The runtime catalog is a sorted snapshot containing a process-generation ID,
|
|
monotonic revision, and the complete set of active plugin tool descriptors.
|
|
The generation ID changes on backend startup. A running agentctl accepts a new
|
|
generation and, within one generation, ignores snapshots older than its applied
|
|
revision. This prevents delayed refresh requests from restoring stale tools.
|
|
|
|
Launch and resume carry the complete current snapshot. Live plugin lifecycle
|
|
changes push a replacement snapshot to every active execution. Agentctl filters
|
|
the snapshot by its backend-owned MCP surface before constructing the effective
|
|
registry.
|
|
|
|
Catalog snapshots are runtime state. The declarations and plugin lifecycle
|
|
status remain persisted in the installed plugin record; no new database entity
|
|
is introduced.
|
|
|
|
## Permissions
|
|
|
|
- Installing, upgrading, enabling, disabling, or uninstalling a plugin retains
|
|
the existing operator/admin permission boundary.
|
|
- Any task agent whose backend-owned surface matches an active declaration may
|
|
discover the tool. There is no per-workspace or per-agent-profile enablement
|
|
setting in this version.
|
|
- Agentctl injects its bound task and session IDs into the backend request. An
|
|
agent cannot select another task or session through arguments.
|
|
- The backend applies the same in-session MCP identity scoping used by built-in
|
|
task tools and verifies that the bound session belongs to the bound task.
|
|
- Plugins remain privileged installed code. The tool declaration does not
|
|
sandbox the plugin process or reduce capabilities already granted by its
|
|
manifest.
|
|
|
|
## Failure Modes
|
|
|
|
- Invalid tool declarations reject install or upgrade without replacing the
|
|
previous installed plugin.
|
|
- A catalog refresh failure is logged per execution and does not roll back the
|
|
plugin lifecycle change. Calls are still rejected against current backend
|
|
state even if a client temporarily retains stale discovery. Launch, resume,
|
|
or the next catalog change repairs the runtime catalog.
|
|
- A call discovered immediately before disable, uninstall, or degradation
|
|
fails closed when backend revalidation observes the new state.
|
|
- Disabling, uninstalling, or replacing the plugin process cancels calls still
|
|
executing in that process. Registry replacement alone does not cancel calls
|
|
to unrelated plugins.
|
|
- An unavailable or crashed plugin returns an MCP tool error. Kandev does not
|
|
retry because the operation may have produced side effects before failure.
|
|
- Invalid arguments are rejected before plugin invocation. Backend validation
|
|
repeats agentctl's shared MCP schema validation as defense in depth.
|
|
- A plugin result that is missing fallback text, exceeds the result limit, or
|
|
violates its declared output schema becomes an MCP tool error.
|
|
- Plugin panics or transport failures follow the existing supervised plugin
|
|
error/recovery lifecycle; recovery republishes the tool catalog.
|
|
|
|
## Persistence Guarantees
|
|
|
|
- Tool declarations survive restart as part of the installed manifest record.
|
|
- Plugin active/disabled/error status follows the existing plugin lifecycle
|
|
persistence contract.
|
|
- Runtime catalog generation, revision, and per-agentctl applied snapshots do
|
|
not survive restart. They are rebuilt from installed plugin records.
|
|
- In-flight calls are not resumed after backend, agentctl, agent, or plugin
|
|
process restart.
|
|
|
|
## Scenarios
|
|
|
|
- **GIVEN** an active plugin declaring a valid Kanban tool, **WHEN** a Kanban
|
|
task agent initializes MCP, **THEN** `tools/list` includes the derived tool
|
|
name, description, schemas, and conservative or declared annotations.
|
|
- **GIVEN** the same plugin tool targets only `kanban-task`, **WHEN** an Office,
|
|
Configuration, or External MCP client lists tools, **THEN** that tool is not
|
|
present.
|
|
- **GIVEN** a running compatible task agent, **WHEN** an operator enables a
|
|
plugin with a matching tool, **THEN** the MCP connection receives one
|
|
`tools/list_changed` notification and the next `tools/list` includes it
|
|
without restarting the agent process.
|
|
- **GIVEN** a running task agent has discovered a plugin tool, **WHEN** the
|
|
plugin is disabled before invocation, **THEN** the call fails without
|
|
entering the plugin and the refreshed list excludes the tool.
|
|
- **GIVEN** a valid call, **WHEN** the plugin executes it, **THEN** the plugin
|
|
receives the exact arguments plus Kandev-bound invocation, task, session,
|
|
workspace, and surface context and the agent receives fallback text and any
|
|
valid structured content.
|
|
- **GIVEN** an agent supplies another task or session ID as ordinary tool
|
|
arguments, **WHEN** it invokes the tool, **THEN** those values do not replace
|
|
the context bound by Kandev.
|
|
- **GIVEN** invalid arguments, **WHEN** the agent invokes a plugin tool, **THEN**
|
|
Kandev returns a schema-validation error and the plugin receives no RPC.
|
|
- **GIVEN** a plugin call times out or its process crashes, **WHEN** the request
|
|
completes, **THEN** the agent receives one MCP error and Kandev does not retry
|
|
the call.
|
|
- **GIVEN** two catalog snapshots arrive out of order, **WHEN** agentctl has
|
|
already applied the newer revision from the same generation, **THEN** it
|
|
ignores the older snapshot and does not restore removed tools.
|
|
- **GIVEN** a client ignores `tools/list_changed`, **WHEN** the plugin catalog
|
|
changes, **THEN** Kandev keeps the agent running and backend invocation still
|
|
enforces the current catalog; reconnecting exposes the current list.
|
|
|
|
## Out of Scope
|
|
|
|
- A plugin-hosted MCP server or transparent federation of arbitrary MCP
|
|
resources, prompts, roots, sampling, or elicitation.
|
|
- Plugin tools on Configuration or External MCP surfaces.
|
|
- Per-workspace, per-workflow, per-agent-profile, or per-tool enablement UI.
|
|
- User approval workflows beyond annotations interpreted by the connected MCP
|
|
client.
|
|
- Streaming tool results, progress notifications, binary/image/audio content,
|
|
embedded resources, or resumable calls.
|
|
- Automatic retries or exactly-once execution guarantees.
|
|
- Sandboxing installed plugin binaries or native UI bundles.
|