Skip to main content
Extensions

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.

Loading Local Plugins

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("skills"))

    plugins = await client.list_plugins()
    print(plugins)
You can pass multiple local plugins at once; multiple --plugin-dir arguments will be written in order:
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. To read the complete plugin inventory, call client.list_plugins().

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.

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>.
async with QoderSDKClient(options) as client:
    info = await client.get_server_info()
    for cmd in info.get("commands", []):
        print(cmd["name"], cmd.get("description"))

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:
async with QoderSDKClient(options) as client:
    for agent in client.supported_agents():
        print(agent["name"], agent.get("description"))

Plugin-contributed Skills

skills/<name>/SKILL.md files in a plugin are registered with a plugin-qualified name (plugin:skill). Use options.skills to control their main-session context visibility and invocation policy: pass a qualified name to enable one skill, pass "all" to enable every discovered skill, or omit it to use the CLI's default policy. See Skills documentation.

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():
async with QoderSDKClient(options) as client:
    status = await client.get_mcp_status()
    for server in status["mcpServers"]:
        print(server["name"], server["status"])

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.
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"}],
)

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.
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.

Options Reference

FieldTypeDescription
pluginslist[SdkPluginConfig]Loads local plugin directories; currently the common form is {"type": "local", "path": ...}
settingsstr | Path | dict[str, Any] | NoneSettings passed to the CLI, which can include fields like enabledPlugins, pluginConfigs, etc.
setting_sourceslist[Literal["user", "project", "local"]] | NoneControls which settings sources the CLI reads
Use settings.enabledPlugins to control plugin enablement and settings.pluginConfigs to provide plugin configuration.

Return Value Reference

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

client.get_server_info() returns initialization resources discovered for the current session, including commands, agents, and skills. It has no stable plugin-inventory field; see SDKControlInitializeResponse for the full return type.
{
    "commands": [
        {"name": "plugin-a:greet", "description": "...", "argumentHint": "..."},
        ...
    ],
    "agents": [
        {"name": "plugin-a:helper", "description": "...", "model": "sonnet"},
        ...
    ],
    "skills": [
        {"name": "plugin-a:echo", "description": "...", "source": "plugin"},
        ...
    ],
    # Also includes models / account / output_style and other fields
}

client.list_plugins()

plugins = await client.list_plugins()
Returns the current CLI plugin inventory and each plugin's resource summary. Host UIs should use this method for plugin inventory instead of reading get_server_info()['plugins']. Each item is a PluginDetails object with id, name, source, path, version, scope, enabled, canDisable, and a resources summary grouped into skills, agents, mcpServers, commands, and hooks.

client.reload_plugins()

Returns ReloadPluginsResponse:
{
    "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
}

Best Practices

  • Separate initialization resources from plugin inventory: Use get_server_info() for discovered commands, agents, and skills; use list_plugins() for plugin inventory. Skills in the initialization result are a discovery inventory, not every skill currently invokable in the main session.
  • 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.

Current Limitations

  • 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.