PluginsBring Your Own Tools via MCP

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):

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

Shape 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 server

Lifecycle

  • 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/url expand 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:

SignalTransport
commandstdio (subprocess)
URL starts with ws:// / wss://WebSocket
URL contains /sseSSE
URL otherwiseStreamable HTTP (POST + SSE response)

Preflight validation

bahulam validate checks:

  • Every mcpServers entry has EITHER command (stdio) OR url (remote) — never both, never neither
  • args is an array when present
  • Every <server>.<tool> reference in an agent’s tools: 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 serverAny 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 handlerTight 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:

  1. Call palette.describe_color (MCP subprocess)
  2. Call palette.palette_from (MCP subprocess)
  3. Call save_palette (JS handler → writes to state.db → fires SSE)
  4. 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 #7C3AED

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