PicoClaw supports Agent Plugins 1.0.0, including skills, stdio MCP, Streamable HTTP MCP and legacy HTTP+SSE MCP. The 1.1.0 draft is not enabled. The portable contract is the published specification.
Build the included demo from the PicoClaw repository root:
mkdir -p examples/plugins/demo-plugin/bin
go build -o examples/plugins/demo-plugin/bin/demo-plugin ./examples/plugins/demo-plugin
On Windows, build bin/demo-plugin.exe and change the demo’s mcp.json command
to ./bin/demo-plugin.exe.
Add this section to your PicoClaw configuration, replacing the example path:
{
"plugins": {
"enabled": true,
"entries": {
"demo-plugin": {
"enabled": true,
"path": "/absolute/path/to/picoclaw/examples/plugins/demo-plugin"
}
}
}
}
Start PicoClaw or reload its configuration. Ask it to use demo-plugin:greet
to greet Ada. The MCP greet tool returns Hello, Ada!.
The demo uses the Go MCP SDK already present in this repository. No additional package manager, API key or service is required. Compile before enabling it; the host never runs installation scripts or downloads dependencies implicitly.
my-plugin/
├── plugin.json
├── mcp.json # optional
├── skills/ # optional
│ └── greet/
│ ├── SKILL.md
│ └── references/example.md
├── bin/server # optional bundled executable
└── com.sipeed.picoclaw/ # optional PicoClaw extension
└── hooks.json
Minimal plugin.json:
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "my-plugin"
}
The manifest supports version, description, author, homepage,
repository, license, keywords, and extensions. Names use lowercase
ASCII letters, digits, periods and hyphens, are 1–64 characters long, start
and end with an alphanumeric character, and contain neither -- nor ...
Validation is implemented locally in Go against the versioned schema rules and
the specification’s semantic requirements. Reference schemas and their Apache
license are included in pkg/plugins/schemas. Schema URLs select supported
versions; they are never fetched at runtime. Unknown top-level manifest fields
and a non-object extensions field produce warnings and are ignored. Other
manifest schema violations reject the plugin. Unknown extension namespaces are
ignored without inspecting their values. Metadata such as version is not
subjected to additional SemVer or URL constraints absent from the specification.
By default, discovery searches immediate child directories of:
<workspace>/plugins<PICOCLAW_HOME>/plugins (normally ~/.picoclaw/plugins)Copy or clone a reviewed package into one of those directories, or provide an
explicit entries.<id>.path. Discovery alone never activates a package. Both
plugins.enabled and the entry’s enabled must be true.
For the default plugin directory, install a package like this:
mkdir -p ~/.picoclaw/plugins
git clone <plugin-repository> ~/.picoclaw/plugins/<plugin-name>
You can also copy it to <workspace>/plugins/<plugin-name>. Then enable the
plugin in config.json, using the name from its plugin.json as the entry ID:
{
"plugins": {
"enabled": true,
"entries": {
"plugin-name": { "enabled": true }
}
}
}
Restart PicoClaw or reload its configuration to activate the plugin. Review packages before enabling them; local plugin servers run as subprocesses.
An entry ID follows the plugin name syntax. Without path, it selects a
discovered manifest name. With path, it is a stable installation alias and may
differ from the manifest name. Ambiguous names require an explicit path; the
same resolved package root cannot be activated under multiple aliases.
Relative directories and entry paths resolve against the workspace. Use
absolute paths when installing dependencies or enabling the repository demo.
Paths do not use shell expansion; write an absolute home path instead of ~.
{
"plugins": {
"enabled": true,
"directories": ["plugins"],
"data_dir": "/absolute/path/to/plugin-data",
"concurrency": 4,
"init_timeout_ms": 15000,
"call_timeout_ms": 60000,
"startup_wait_ms": 15000,
"entries": {
"my-plugin": {
"enabled": true,
"agents": ["main"],
"allow_hooks": false,
"config": {"format": "short"}
}
}
}
}
data_dir defaults to <PICOCLAW_HOME>/plugin-data; a relative override resolves
against the workspace. Each installation receives a persistent subdirectory
named by its entry ID. Keep the ID unchanged when updating the package.
concurrency limits simultaneous plugin initializations (1–32; default 4).
init_timeout_ms applies to each MCP connection or hook handshake.
call_timeout_ms bounds MCP tool calls. Positive duration overrides are used;
zero or negative durations select the documented defaults.
Omitting agents grants access to all configured agents. "agents": [] grants
none. Agent tools allowlists still apply. Agent mcpServers declarations use
<installation-id>:<server-name>, for example demo-plugin:greeter.
Plugin MCP activation is controlled by plugins, independently of the native
tools.mcp.enabled server list. Existing MCP discovery settings are reused when
a search method is enabled. Runtime tool names have an mcp_plugin_ prefix and
a stable hash suffix to fit provider limits and avoid ambiguous normalization.
Use the emitted tool definitions to obtain exact names for a tool allowlist.
Plugin tools never overwrite built-ins or existing tools.
Each immediate child of skills/ containing a regular SKILL.md is a candidate.
Nested descendants are not independently discovered. Files must follow
Agent Skills, including YAML frontmatter,
a matching directory/name and a nonempty description.
Skills appear as <installation-id>:<skill-name>, preserving the original
frontmatter. Validated skill bodies are snapshots until configuration reload.
Loading or removing plugin skills invalidates the prompt cache.
The host provides a plugin-specific read_resource tool, with a plugin_ name
prefix, for bundled references. Its name is included in the loaded skill
instructions. It accepts only UTF-8 text paths under skills/ or references/,
relative to the plugin root; reads are bounded to 2 MiB and cannot follow links
outside it. Binary assets and files outside those two directories are not
readable. This avoids granting general filesystem access outside the agent’s
workspace. The tool is subject to the agent’s normal tool allowlist.
Put portable MCP configuration in root mcp.json:
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
"mcpServers": {
"local": {
"type": "stdio",
"command": "./bin/server",
"args": ["--data", "${PLUGIN_DATA}"],
"cwd": "${PLUGIN_ROOT}",
"env": {"CONFIG_FILE": "${PLUGIN_ROOT}/config.json"}
},
"remote": {
"type": "streamable-http",
"url": "https://example.org/mcp",
"headers": {"X-Tenant": "public"}
}
}
}
command is a single executable name resolved through the platform search path,
or a bundled executable path beginning with ./. It is never evaluated as a
shell command. Arguments are passed separately. The default working directory is
the plugin root; explicit directories must remain inside the plugin root or the
plugin’s data directory.
PicoClaw supplies PLUGIN_ROOT and writable PLUGIN_DATA. The base environment
contains only available executable-search and temporary-directory variables;
provider keys and other ambient variables are not inherited. Configured env
values overlay this base. The two reserved variables cannot be set by packages.
Only their exact placeholders are expanded, once, in args, env values and
cwd. Other placeholders remain literal. Remote header values additionally
support ${VAR} references to the PicoClaw process environment. No expansion
occurs in commands or remote URLs. Install dependencies into PLUGIN_DATA
explicitly.
Remote URLs require HTTPS except for literal loopback addresses and localhost.
User information, fragments, invalid headers and duplicate case-insensitive
header names are rejected. Client protocol headers take precedence. Redirects
and legacy SSE endpoint events cannot send requests to another origin.
Legacy sse selects the actual HTTP+SSE protocol here; this does not change the
historical interpretation of sse in native PicoClaw MCP configuration.
Literal credentials do not belong in portable package env or headers.
For a remote MCP server, a header in a host mcp_overrides entry can refer to
a host environment variable:
"headers": {"Authorization": "Bearer ${REMOTE_API_KEY}"}
Set REMOTE_API_KEY in the environment of the PicoClaw process. References
are resolved once when the server connects. An unset or empty variable, an
invalid reference, or a resolved header containing invalid HTTP characters
disables that server with a diagnostic that does not contain the secret.
This expansion is host-only. A ${VAR} reference written in a portable package
mcp.json is sent literally and never reads the host environment; only headers
provided through plugins.entries.<id>.mcp_overrides.<server> are expanded.
Review the override’s header references before enabling a plugin: they select
which host environment values are sent to its MCP endpoint. Stdio processes
still receive only their explicitly configured environment.
Alternatively, for a server that accepts a bearer token, put it alone in a
file under that installation’s PLUGIN_DATA directory and configure it on
the host:
"mcp_bearer_token_files": {"remote": "api-key"}
Add this field to plugins.entries.<id>. The map key is the server name in
mcp.json; the value is a path relative to PLUGIN_DATA. PicoClaw reads the
file at connection time and sends Authorization: Bearer <token> only to that
server’s origin. The host value replaces a package Authorization header.
Missing, empty, invalid or escaping files disable that server with a diagnostic;
the token itself is not logged. This host configuration leaves the portable
Agent Plugins manifest unchanged. OAuth is not provided by this mechanism.
Hooks are a PicoClaw extension, not a portable Agent Plugins component. Enable
them explicitly with entries.<id>.allow_hooks: true. A plugin may declare
process hooks or direct MCP actions in the same hooks.json file. Direct MCP
actions use the plugin’s already configured MCP servers and run through
PicoClaw’s normal hook lifecycle.
Each action names a server and tool from mcp.json or the entry’s
mcp_overrides. intercept runs the action at a synchronous hook stage;
observe runs it for a runtime event. Supported intercept stages are
before_llm, after_llm, before_tool, after_tool, and approve_tool.
Observe accepts PicoClaw runtime event names such as agent.turn.end; the
turn_completed alias runs only for successfully completed turns.
Arguments support the event’s available ${user_message} and
${assistant_message} values, ${tool_name}, ${tool_arguments},
${tool_result}, ${request}, ${response}, ${event}, and
${event_payload}. A placeholder occupying the whole value preserves its JSON
type, so ${tool_arguments} can be an object. Dotted paths such as
${event.payload.UserMessage} select nested fields.
The default result mode is ignore, which is useful for capture or other
side-effect tools. append_to_user_message is valid with before_llm; it adds
the MCP result to the prompt inside an untrusted context block. approval is
valid with approve_tool; the MCP tool must return true/false or an object
with an approved boolean. Approval errors fail closed. Other MCP hook errors
are logged and leave the turn running.
This example uses Mem0’s official Agent Plugin for its manifest and skills, Mem0’s hosted MCP endpoint for tool calls, and PicoClaw’s hook mapping for automatic recall and capture. It does not run Mem0’s local MCP server.
Clone the official plugin and the shared plugin core into PicoClaw’s plugin
directory. These commands respect PICOCLAW_HOME when it is set:
PICOCLAW_HOME="${PICOCLAW_HOME:-$HOME/.picoclaw}"
PLUGIN_REPO="$PICOCLAW_HOME/plugins/mem0-source"
PLUGIN_ROOT="$PLUGIN_REPO/integrations/mem0-agent-plugin"
mkdir -p "$PICOCLAW_HOME/plugins"
git clone --depth 1 --filter=blob:none --sparse \
https://github.com/mem0ai/mem0.git "$PLUGIN_REPO"
git -C "$PLUGIN_REPO" sparse-checkout set \
integrations/mem0-agent-plugin integrations/agent-plugin-core
mkdir -p "$PLUGIN_ROOT/com.sipeed.picoclaw"
cat > "$PLUGIN_ROOT/com.sipeed.picoclaw/hooks.json" <<'JSON'
{
"hooks": [
{
"name": "memory-recall",
"intercept": ["before_llm"],
"mcp": {
"server": "mem0",
"tool": "search_memories",
"arguments": {
"query": "${user_message}",
"filters": {"AND": [{"user_id": "your-user-id"}, {"app_id": "your-project-id"}]},
"top_k": 5
},
"result": "append_to_user_message"
}
},
{
"name": "memory-capture",
"observe": ["turn_completed"],
"mcp": {
"server": "mem0",
"tool": "add_memory",
"arguments": {
"text": "User: ${user_message}\nAssistant: ${assistant_message}",
"user_id": "your-user-id",
"app_id": "your-project-id"
}
}
}
]
}
JSON
Store the Mem0 API key in the plugin’s private data directory. The prompt hides
the key while you paste it; the file is created with mode 0600:
python3 - <<'PY'
import os
from getpass import getpass
from pathlib import Path
home = Path(os.environ.get("PICOCLAW_HOME", str(Path.home() / ".picoclaw"))).expanduser()
key_file = home / "plugin-data" / "mem0" / "mem0-api-key"
key_file.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
key_file.parent.chmod(0o700)
key_file.write_text(getpass("Mem0 API key: ").strip())
key_file.chmod(0o600)
print(f"Saved key to {key_file}")
PY
Merge these settings into config.json. Set path to the absolute path of the
cloned plugin. Keep any existing tools.mcp.servers entries; the discovery
settings shown here leave discovery enabled globally, while deferred: false
exposes Mem0’s tools directly on every model call:
{
"tools": {
"mcp": {
"enabled": true,
"discovery": {
"enabled": true,
"use_bm25": true,
"use_regex": false
}
}
},
"plugins": {
"enabled": true,
"entries": {
"mem0": {
"enabled": true,
"path": "/absolute/path/to/mem0-source/integrations/mem0-agent-plugin",
"allow_hooks": true,
"mcp_overrides": {
"mem0": {
"type": "streamable-http",
"url": "https://mcp.mem0.ai/mcp",
"deferred": false
}
},
"mcp_bearer_token_files": {
"mem0": "mem0-api-key"
}
}
}
}
}
Since skill_names and agents are omitted, PicoClaw loads all plugin skills
and enables the plugin for all configured agents. The token script assumes the
default plugin data directory; if plugins.data_dir is configured, save the key
under <plugins.data_dir>/mem0/mem0-api-key instead.
The official Mem0 Agent Plugin
ships a local MCP server; this configuration replaces it with Mem0’s
hosted MCP server. The host-side
deferred: false option does not modify the portable plugin’s mcp.json.
Other MCP servers continue to follow the global discovery setting unless they
have their own per-server override. Never put the API key literally in
config.json, mcp.json, or hooks.json.
Restart PicoClaw after saving the files. Ask it to remember a harmless fact,
then ask about that fact in a later turn. The completed turn triggers
add_memory; the following turn’s before_llm hook calls search_memories and
adds the result to the model context. With tool feedback enabled, these
hook-triggered MCP calls are also shown in the conversation.
search_memories on Mem0’s hosted MCP server accepts top_k, the maximum
number of memories returned. Its default is 10, so set it explicitly to bound
how much memory context each turn injects. The hook above uses top_k: 5;
values of 2–3 work well for short, focused turns:
"arguments": {
"query": "${user_message}",
"filters": {"AND": [{"user_id": "your-user-id"}, {"app_id": "your-project-id"}]},
"top_k": 3
}
Do not use limit: the hosted server ignores it and returns its default of 10.
The Mem0 Platform API reference also lists threshold, but the hosted v3 search
endpoint ignores it, so it does not filter results. To reduce low-relevance
noise, lower top_k rather than relying on threshold; use the plugin’s local
MCP server or filter in the agent when you need score-based filtering.
Filters accept a created_at range, for example
{"AND": [{"user_id": "your-user-id"}, {"created_at": {"gte": "2026-01-01"}}]},
to prefer recent memories.
The same hook declarations work with other MCP providers by changing the
server, tool names and arguments. Supermemory, for example, uses
search_memory(query, containerTag) and
add_memory(content, action, containerTag). Its hosted endpoint supports
OAuth with picoclaw mcp login <plugin-id>:<server-name>; API-key bearer
authentication also works. A real API test confirmed recall and capture with
Supermemory on 5 October 2026.
MCP arguments are filtered for configured secrets and the target server’s
authentication values. The ${user_message} and ${assistant_message} shortcuts
are bounded to 6,000 bytes each; context added to the prompt is bounded to
12,000 bytes. Each agent’s MCP server allowlist still applies.
Process hook entries can share hooks.json with MCP actions. Put the hooks
object either inline in plugin.json or in com.sipeed.picoclaw/hooks.json;
using both is an error limited to the hook extension.
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "my-plugin",
"extensions": {
"com.sipeed.picoclaw": {
"hooks": [{
"name": "observer",
"command": "python3",
"args": ["${PLUGIN_ROOT}/com.sipeed.picoclaw/observer.py"],
"observe": ["agent.turn.end"],
"intercept": ["after_llm"]
}]
}
}
}
Process launch fields follow the same validation, environment and containment
rules as stdio MCP. The process implements PicoClaw’s existing JSON-RPC hook
protocol: hook.hello, hook.runtime_event, and the requested interception
methods. entries.<id>.config is delivered as config in the hello payload.
Only declared stages and allowed agents are forwarded.
See the hook protocol. Hook failures use the
existing hook manager’s timeout and error policies. In particular, approval
errors can deny a tool call; they do not crash the runtime. Trusted interception
hooks can alter or short-circuit execution, so allow_hooks is a separate grant.
Portable MCP tools always use the normal registry/approval execution path.
Gateway loading runs asynchronously. Direct requests and heartbeats wait up to
startup_wait_ms; unfinished plugins continue loading in the background.
Skills are published before MCP initialization. A bad manifest rejects its
plugin; a bad MCP document disables that component type; a bad server or skill
is skipped independently. Other plugins and components continue loading.
AgentLoop.PluginStatuses() exposes initialization state and diagnostics to host
integrations. States include discovered, loading, ready, degraded, failed and
closed. Runtime tool failures are returned as tool errors and logged. Failed
calls are not automatically replayed; reload explicitly to reconnect a crashed
plugin. Shutdown and reload cancel calls, join loaders, remove registrations
and close subprocesses. Data directories survive reload and package updates.
Plugin subprocesses use pkg/isolation just like existing MCP servers and
process hooks. Linux and Windows use the configured platform backends. Other
platforms, including macOS, do not gain a new OS sandbox from this feature.
When isolation is disabled, enabled executables run with PicoClaw’s OS user
permissions. Path containment protects package discovery and resource access;
it is not a sandbox for arbitrary subprocess code. Enable only trusted packages.
For isolated installations outside the instance filesystem, configure explicit read-only package and writable data/toolchain exposure as described in the isolation documentation. The plugin loader never silently broadens those OS permissions.
To disable a plugin, set its entry’s enabled to false and reload. To uninstall,
disable it first, then remove its package directory. Remove its dedicated data
directory separately only if you want to discard persisted state.
Implement tools with an MCP SDK in any language. Keep stdout exclusively for the
stdio protocol and log to stderr. Declare accurate input schemas and return MCP
tool errors for expected failures. Keep the portable manifest free of custom
tools, hooks or runtime fields; use MCP and the extension namespace instead.
Run the focused Go tests from the repository root:
go test -tags stdjson ./pkg/plugins ./pkg/mcp ./pkg/skills ./pkg/config ./pkg/agent ./pkg/tools
go test -race -tags stdjson ./pkg/plugins ./pkg/mcp ./pkg/skills ./pkg/config ./pkg/agent ./pkg/tools
HTTP integration tests require local loopback listeners. Tests exercise a real stdio child process, HTTP and legacy SSE, manifest and component failure boundaries, environment expansion, paths, timeouts, crash isolation and reload.