Bring Your Own Tools via MCP
You are not restricted to JavaScript handlers. Anything already speaking Model Context Protocol — Python, Go, Rust, Swift, remote HTTP services — plugs into a Bahulam plugin as a first-class tool source. A plugin becomes the product wrapper around one or more MCP servers, adding the pieces MCP alone doesn’t give you: curated agents, workspace views, and shared persistent state.
Plugin = MCP + UX. That’s the whole reframe: MCP handles cross-language tools, we handle the packaging + human-facing layer.
Declaring MCP servers
Two shapes, both merged (inline wins on collision):
Shape 1 — inline in plugin.yaml (recommended for new plugins)
apiVersion: bahulam.plugin/1
metadata:
name: options-desk
config:
mcpServers:
quant-engine: # Python via uvx
command: uvx
args: [options-quant-mcp]
chain-fetcher: # remote MCP over SSE
url: https://mcp.tradeco.com/sse
headers:
Authorization: Bearer ${MCP_TRADECO_TOKEN}
code-index: # any local binary
command: /usr/local/bin/mcp-code-index
args: [--repo, "${WORKSPACE_ROOT}"]
workspace: ./config/workspace.yamlShape 2 — sibling mcp.json (drop-in from existing configs)
plugins/options-desk/
├── plugin.yaml
├── mcp.json ← same format as Claude Desktop / Cursor / Cline
├── tools/
└── workspace/{
"mcpServers": {
"quant-engine": { "command": "uvx", "args": ["options-quant-mcp"] }
}
}An MCP config that already works in Claude Desktop drops in as-is. No YAML
translation needed. The plugin.yaml just references the tools by
<server>.<tool> name.
Namespacing rule
MCP tools are surfaced as <server>.<tool> in the tool list. That means two
different plugins can each ship an MCP server with a search tool and they
won’t collide — the agent sees elastic.search and pinecone.search as
distinct names.
Agent tools: lists mix all three sources freely. Keep the agent itself in
the canonical backend-compatible YAML shape:
apiVersion: agent.framework/v1
kind: SingleAgent
metadata:
slug: analyst
name: Options Analyst
role: specialist
agent:
system_prompt: |
Price and explain option strategies using the declared tools.
tools:
- read_file # platform tool
- save_backtest # this plugin's JS tool
- quant-engine.price # MCP tool from this plugin's server
- quant-engine.greeks # another MCP tool from the same server
- chain-fetcher.get_chain # MCP tool from a different serverLifecycle
- Servers are spawned when the plugin is scoped in (
bahulam plugin <name>or when the plugin is enabled in a general workspace session) - Torn down when the workspace closes — no orphan processes
- Failure of one MCP server logs a warning; the plugin’s JS tools and views keep working. Never all-or-nothing.
- Environment variables in
command/args/env/headers/urlexpand from the user’s shell at spawn time (${VAR}syntax) - Servers declared by a plugin are scoped to that plugin’s session — they never pollute the user’s global MCP list
Transports auto-detected from the shape of the config:
| Signal | Transport |
|---|---|
command | stdio (subprocess) |
URL starts with ws:// / wss:// | WebSocket |
URL contains /sse | SSE |
| URL otherwise | Streamable HTTP (POST + SSE response) |
Preflight validation
bahulam validate checks:
- Every
mcpServersentry has EITHERcommand(stdio) ORurl(remote) — never both, never neither argsis an array when present- Every
<server>.<tool>reference in an agent’stools:list points at a declared server (the<tool>half is validated live when the server starts, since preflight can’t spawn every server)
When to use MCP vs a JS handler
| Choose… | For |
|---|---|
| MCP server | Any language other than JS; existing MCP server you want to wrap; heavy compute; a tool you also want to expose to other MCP clients (Claude Desktop, Cursor); tools that need OS-level access without touching the CLI process |
| JS handler | Tight Shared Blackboard integration where write latency matters; small logic that doesn’t warrant a subprocess; anything that needs options.state synchronously |
The hello-mcp
reference plugin uses both — an MCP server for the compute, one JS tool for
state — so an agent can:
- Call
palette.describe_color(MCP subprocess) - Call
palette.palette_from(MCP subprocess) - Call
save_palette(JS handler → writes to state.db → fires SSE) - The workspace view sees the SSE event and re-renders
Install and try:
bahulam install hello-mcp
bahulam plugin hello-mcp . # open the workspace scoped to this plugin
# then ask the agent:
# design 3 palettes seeded from #0891B2, #F97316, and #7C3AEDWhat this doesn’t lock you into
Nothing prevents you from writing a plugin that’s 100% JS tools with no MCP, or 100% MCP with a JS-only workspace view and no JS tools at all. Pick per tool. Mix within a single plugin. The CLI treats all three (built-in, JS, MCP) as one flat tool list to the agent.