> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qoder.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Plugins

`options.plugins` is used to load local plugin directories into the current session. The SDK converts each local plugin into a `--plugin-dir <path>` startup argument; commands, agents, skills, and MCP servers contained in the plugin all take part in capability discovery for the session.

<div id="loading-local-plugins" />

<div id="loadinglocalplugins" />

## Loading Local Plugins

```python theme={null}
from qoder_agent_sdk import QoderAgentOptions, QoderSDKClient

options = QoderAgentOptions(
    plugins=[
        {"type": "local", "path": "/path/to/my-plugin"},
    ],
)

async with QoderSDKClient(options) as client:
    info = await client.get_server_info()
    print(info.get("commands"))
    print(info.get("agents"))
    print(info.get("plugins"))
```

You can pass multiple local plugins at once; multiple `--plugin-dir` arguments will be written in order:

```python theme={null}
options = QoderAgentOptions(
    plugins=[
        {"type": "local", "path": "/path/to/plugin-a"},
        {"type": "local", "path": "/path/to/plugin-b"},
    ],
)
```

> 💡 `query()` is a one-shot message stream; it cannot query the init response after the handshake. To read commands / agents / skills contributed by plugins, use `QoderSDKClient` and call `get_server_info()` after `connect()`, or capture `SystemMessage(subtype='init')` in the message stream and read `message.data` yourself.

<div id="plugin-directory-layout" />

<div id="plugindirectorylayout" />

## Plugin Directory Layout

A local plugin typically contains:

```
my-plugin/
  .qoder-plugin/plugin.json
  commands/
  agents/
  skills/
  .mcp.json
```

`.qoder-plugin/plugin.json` declares the plugin name, version, and description. The other directories are automatically scanned by the CLI based on file type.

The SDK does not validate whether the path exists or is well-formed:

* A non-existent `--plugin-dir` path is silently ignored in SDK mode; the session still initializes normally.
* Broken frontmatter or `.mcp.json` does not block init; broken commands simply do not appear in the init response.
* To explicitly diagnose plugin loading failures, the only fallback right now is [`reload_plugins().error_count`](#reloading-plugins-at-runtime).

<div id="plugin-contributed-slash-commands" />

<div id="plugincontributedslashcommands" />

## Plugin-contributed Slash Commands

`commands/*.md` files in a plugin appear in `get_server_info()['commands']`, with names in the plugin-qualified form `<plugin>:<cmd>`.

```python theme={null}
async with QoderSDKClient(options) as client:
    info = await client.get_server_info()
    for cmd in info.get("commands", []):
        print(cmd["name"], cmd.get("description"))
```

<div id="plugin-contributed-agents" />

<div id="plugincontributedagents" />

## Plugin-contributed Agents

`agents/*.md` files in a plugin appear in `get_server_info()['agents']`. The SDK also provides a synchronous convenience method for getting this list directly:

```python theme={null}
async with QoderSDKClient(options) as client:
    for agent in client.supported_agents():
        print(agent["name"], agent.get("description"))
```

<div id="plugin-contributed-skills" />

<div id="plugincontributedskills" />

## Plugin-contributed Skills

`skills/<name>/SKILL.md` files in a plugin are registered with a plugin-qualified name (`plugin:skill`). To make them callable in the main session, list them explicitly in `options.skills`; see [Skills documentation](/en/cli/sdk/python/skills#enabling-plugin-skills).

<div id="plugin-contributed-mcp-servers" />

<div id="plugincontributedmcpservers" />

## Plugin-contributed MCP Servers

`.mcp.json` in a plugin is started by the CLI and included in the MCP status, which can be read via `get_mcp_status()`:

```python theme={null}
async with QoderSDKClient(options) as client:
    status = await client.get_mcp_status()
    for server in status["mcpServers"]:
        print(server["name"], server["status"])
```

<div id="temporarily-overriding-an-installed-plugin-with-the-same-name" />

<div id="temporarilyoverridinganinstalledpluginwiththesamename" />

## Temporarily Overriding an Installed Plugin with the Same Name

Local plugins loaded through `options.plugins` are session-scoped. During the current session, if a local plugin shares a name with an installed plugin, the local plugin takes priority in capability discovery for the session. This is useful for plugin development, debugging, and canary testing.

```python theme={null}
options = QoderAgentOptions(
    # The local version only takes effect for this query session and does not
    # touch the user's global install state.
    plugins=[{"type": "local", "path": "./my-plugin-dev"}],
)
```

<div id="reloading-plugins-at-runtime" />

<div id="reloadingpluginsatruntime" />

## Reloading Plugins at Runtime

If the plugin directory changes, you can call `reload_plugins()` within the same `QoderSDKClient` session to have the CLI rescan plugin resources.

```python theme={null}
async with QoderSDKClient(options) as client:
    refreshed = await client.reload_plugins()

    print(refreshed["commands"])
    print(refreshed["agents"])
    print(refreshed["plugins"])
    print(refreshed["mcpServers"])
    print(refreshed["error_count"])
```

Typical use cases:

* Refreshing after adding or deleting `commands/*.md` during plugin development.
* After installing or updating a local plugin without restarting the host application.
* When the host UI needs to display commands, agents, plugins, and MCP status after a reload.

> Note: `reload_plugins()` is only meaningful in `QoderSDKClient` (streaming) mode; the one-shot `query()` stream has no runtime control channel.

<div id="options-reference" />

<div id="optionsreference" />

## Options Reference

| Field             | Type                                                | Description                                                                                       |
| ----------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `plugins`         | `list[SdkPluginConfig]`                             | Loads local plugin directories; currently the common form is `{"type": "local", "path": ...}`     |
| `settings`        | `str \| Path \| dict[str, Any] \| None`             | Settings passed to the CLI, which can include fields like `enabledPlugins`, `pluginConfigs`, etc. |
| `setting_sources` | `list[Literal["user", "project", "local"]] \| None` | Controls which settings sources the CLI reads                                                     |

Enterprise policy fields like `settings.enabledPlugins`, `settings.pluginConfigs`, `settings.allowedChannelPlugins`, and `settings.strictPluginOnlyCustomization` are passed through by the SDK types, but the actual pipeline depends on the specific CLI version. Verify against your environment before use.

<div id="return-value-reference" />

<div id="returnvaluereference" />

## Return Value Reference

<div id="clientget_server_info-and-systemmessagesubtypeinitdata" />

<div id="clientget_server_infoandsystemmessagesubtypeinitdata" />

### `client.get_server_info()` and `SystemMessage(subtype='init').data`

```python theme={null}
{
    "commands": [
        {"name": "plugin-a:greet", "description": "...", "argumentHint": "..."},
        ...
    ],
    "agents": [
        {"name": "plugin-a:helper", "description": "...", "model": "sonnet"},
        ...
    ],
    "skills": [
        {"name": "plugin-a:echo", "description": "...", "source": "plugin"},
        ...
    ],
    "plugins": [
        {"name": "plugin-a", "path": "/path/to/plugin-a", "source": "local"},
        ...
    ],
    # Also includes models / account / output_style and other fields
}
```

<div id="clientreload_plugins" />

<div id="client-reload_plugins" />

### `client.reload_plugins()`

Returns `ReloadPluginsResponse`:

```python theme={null}
{
    "commands": list[Any],
    "agents": list[Any],
    "plugins": list[PluginInfo],            # {"name": str, "path": str, "source": str?}
    "mcpServers": list[McpServerStatus],    # See McpServerStatus in mcp.md
    "error_count": int,                     # Number of plugins that failed to load in this reload
}
```

<div id="best-practices" />

<div id="bestpractices" />

## Best Practices

* **Read `get_server_info()` first for the host UI**: It is the stable entry point for displaying commands, agents, skills, and plugins. If a one-shot `query()` needs this data, switch to `QoderSDKClient`, or read `SystemMessage(subtype='init').data` from the message stream.
* **Use `options.plugins` during plugin development**: It only affects the current session and does not require modifying the user's global install state.
* **Prepare user prompts before reloading**: `reload_plugins()` triggers the CLI to rescan disk, which may briefly change the available resource list; synchronize UI updates accordingly.
* **Check `error_count` for diagnostics**: If `error_count > 0` after a reload, some plugin resources failed to load; display the source to the user.

<div id="current-limitations" />

<div id="currentlimitations" />

## Current Limitations

* In some qodercli versions, local plugin commands, agents, and MCP can appear correctly in the initialization result, but plugin skills may not appear in `get_server_info()['skills']` — this is a CLI-side discovery pipeline issue.
* Under the current qodercli implementation, non-existent `--plugin-dir` paths are silently ignored in SDK mode; to explicitly diagnose plugin loading failures, the only fallback right now is `reload_plugins().error_count`.
* `reload_plugins()` is a runtime control API exposed by `QoderSDKClient`; if the current CLI version returns internal errors related to `this._plugins`, an upgrade to a fixed qodercli is needed.
* `settings.strictPluginOnlyCustomization`, `settings.allowedChannelPlugins`, and template substitution in `settings.pluginConfigs.<pid>.options` are dict pass-through fields. The SDK does not parse them; whether they take effect depends entirely on the CLI version.
