Second-pass cleanup after the monolith split. Addresses every issue
from the code-review pass.
Core additions in src/api.ts:
- toMcpResult(data) + toMcpText(text): single source of truth for the
MCP text-content envelope (was ~87 duplicated literals)
- ApiError type + isApiError(v) guard: typed discriminated-union for
the error-by-value pattern; replaces open-coded shape checks
- apiCall<T = unknown>: generic so callers can document expected
response shape without unchecked "as" casts
Bulk cleanups across all 12 tools/*.ts:
- Every handler now returns toMcpResult(data) or toMcpText(text)
- Open-coded "typeof obj === 'object' && 'error' in obj" in
remote_agents.ts replaced with isApiError(v)
- Extracted initialCanvasPosition() helper out of
handleCreateWorkspace; explains why random seeding exists
- Added runtime/workspace_dir/workspace_access to create_workspace
zod schema (previously accepted by handler but hidden from clients)
src/index.ts:
- Replaced "export * from" with explicit named re-exports so the
public surface is auditable and future name collisions fail loudly
Tests:
- createServer() smoke test that records every srv.tool(...) call and
asserts 87 registered tools unique by name. Catches future PRs that
forget to wire a registerXxxTools(srv).
Docs:
- Fix broken relative links in sdk/python/molecule_agent/README.md
(was ../../examples/ from inside sdk/python/, should be ../examples/)
- Update stale "61 tools" -> "87 tools" in CLAUDE.md + main() log
Verification:
- npm run build clean
- npx jest -> 97/97 passed (was 96; +1 smoke test)
- grep "content: [{ type: \"text\" as const" src/tools/ -> 0 matches
- No file over 216 lines
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| examples/remote-agent | ||
| molecule_agent | ||
| molecule_plugin | ||
| template | ||
| tests | ||
| pyproject.toml | ||
| pytest.ini | ||
| README.md | ||
molecule_plugin — Python SDK for building Molecule AI plugins
A Molecule AI plugin is a directory that bundles rules, skills, and per-runtime install adaptors. Any plugin that conforms to this contract is installable on any Molecule AI workspace whose runtime the plugin supports.
Quick start
Copy template/ to a new directory and edit:
my-plugin/
├── plugin.yaml # name, version, runtimes, description
├── rules/my-rule.md # optional — appended to CLAUDE.md at install
├── skills/my-skill/
│ ├── SKILL.md # instructions injected into the system prompt
│ └── tools/do_thing.py # optional LangChain @tool functions
└── adapters/
├── claude_code.py # one-liner: `from molecule_plugin import AgentskillsAdaptor as Adaptor`
└── deepagents.py # same
Validate:
from molecule_plugin import validate_manifest
errors = validate_manifest("my-plugin/plugin.yaml")
assert not errors, errors
CLI
The SDK ships a CLI for validating Molecule AI artifacts before publishing:
python -m molecule_plugin validate plugin my-plugin/
python -m molecule_plugin validate workspace workspace-configs-templates/claude-code-default/
python -m molecule_plugin validate org org-templates/molecule-dev/
python -m molecule_plugin validate channel channels.yaml
python -m molecule_plugin validate my-plugin/ # kind defaults to 'plugin'
Exit code is 0 when valid, 1 when any errors are found — suitable for CI.
Add -q / --quiet to suppress success lines and emit only errors.
Programmatic equivalents:
from molecule_plugin import (
validate_plugin,
validate_workspace_template,
validate_org_template,
validate_channel_file,
validate_channel_config,
)
Per-runtime adaptors — when to write a custom one
The default AgentskillsAdaptor handles the common shape: rules go into
the runtime's memory file (CLAUDE.md), skill dirs go into /configs/skills/.
That covers most plugins.
Write a custom adaptor when you need to:
- Register runtime tools dynamically — call
ctx.register_tool(name, fn). - Register DeepAgents sub-agents — call
ctx.register_subagent(name, spec). - Write to a non-standard memory file — call
ctx.append_to_memory(filename, content).
Minimum custom adaptor:
# adapters/deepagents.py
from molecule_plugin import InstallContext, InstallResult
class Adaptor:
def __init__(self, plugin_name: str, runtime: str):
self.plugin_name, self.runtime = plugin_name, runtime
async def install(self, ctx: InstallContext) -> InstallResult:
ctx.register_subagent("my-agent", {"prompt": "...", "tools": [...]})
return InstallResult(plugin_name=self.plugin_name, runtime=self.runtime, source="plugin")
async def uninstall(self, ctx: InstallContext) -> None:
pass
Resolution order (understood by the platform)
For (plugin_name, runtime):
- Platform registry —
workspace-template/plugins_registry/<plugin>/<runtime>.py(curated; set by the Molecule AI team for quality-assured plugins). - Plugin-shipped —
<plugin_root>/adapters/<runtime>.py(what this SDK helps you build). - Raw-drop fallback — copies plugin files into
/configs/plugins/<name>/and surfaces a warning; no tools are wired.
You generally ship for path #2. If your plugin becomes popular enough to be promoted to "default," the Molecule AI team PRs a copy of your adaptor into the platform registry (path #1) so it survives upstream breakage.
Testing locally
The SDK ships AgentskillsAdaptor as a standalone, unit-testable class:
import asyncio
from pathlib import Path
from molecule_plugin import AgentskillsAdaptor, InstallContext
ctx = InstallContext(
configs_dir=Path("/tmp/configs"),
workspace_id="local",
runtime="claude_code",
plugin_root=Path("./my-plugin"),
)
asyncio.run(AgentskillsAdaptor("my-plugin", "claude_code").install(ctx))
# check /tmp/configs/CLAUDE.md, /tmp/configs/skills/
Publishing
A plugin is just a directory. Push it to any Git host. Installation via
POST /plugins/install {git_url} is on the roadmap — see the platform's
PLAN.md under "Install-from-GitHub-URL flow." Until then, plugins are
bundled into the platform by dropping them into plugins/ at deploy time.
Supported runtimes
As of 2026-Q2: claude_code, deepagents, langgraph, crewai, autogen,
openclaw. See the live list with:
curl $PLATFORM_URL/plugins