Skip to content

Addon Classpath Discovery

Pedro Gomes Branquinho edited this page Feb 15, 2026 · 1 revision

Addon Classpath Discovery

Status: Implemented (2026-02-13) Implements: ADR-0007

Overview

Addons are automatically discovered on the JVM classpath at startup via META-INF/hive-addons/*.edn manifest files. No hive-mcp code changes are needed to add or remove addons.

This is the same pattern used by Java's ServiceLoader (META-INF/services/), SLF4J, and Spring Boot auto-configuration.

How It Works

startup
  |
  v
scan-classpath-manifests
  -> ClassLoader.getResources("META-INF/hive-addons")
  -> finds *.edn across ALL JARs and directories
  -> parse + validate via Malli schema
  -> topological sort by :addon/dependencies
  |
  v
for each discovered manifest (dependency order):
  try-call-initializer(init-ns)       <- tries init-as-addon!, falls back to init!
  |
  v  (if init-ns fails)
  init-from-manifest!(manifest)       <- resolve constructor, call it, register IAddon
  |
  v
hardcoded extension-namespaces       <- kept as final fallback (backward compat)
  |
  v
manifest gap-fill                    <- extension registry resolution for remaining keys

Writing an Addon

1. Project Structure

my-addon/
  deps.edn
  src/my_addon/init.clj
  resources/
    META-INF/
      hive-addons/
        my-addon.edn          <- manifest (auto-discovered)

2. Manifest Format

;; resources/META-INF/hive-addons/my-addon.edn
{:addon/id           "acme.my-addon"
 :addon/type         :native              ;; :native | :mcp-bridge | :external
 :addon/init-ns      "my-addon.init"
 :addon/init-fn      "init-as-addon!"
 :addon/capabilities #{:tools :schema}
 :addon/dependencies #{"acme.other-addon"}  ;; optional, controls load order
 :addon/description  "What this addon does"
 :addon/version      "1.0.0"              ;; optional
 :addon/config       {:timeout-ms 5000}}  ;; optional, supports ${ENV_VAR} expansion

Required fields: :addon/id, :addon/type, :addon/init-ns, :addon/init-fn

3. deps.edn

{:paths ["src" "resources"]     ;; "resources" MUST be in :paths
 :deps  {io.github.hive-agi/hive-mcp {:git/tag "v0.X.0" :git/sha "..."}}}

4. Implement IAddon

(ns my-addon.init
  (:require [hive-mcp.addons.protocol :as proto]))

(defrecord MyAddon [config]
  proto/IAddon
  (addon-id [_] "acme.my-addon")
  (addon-type [_] :native)
  (capabilities [_] #{:tools})
  (initialize! [_ opts] {:success? true :errors []})
  (shutdown! [_] {:success? true :errors []})
  (tools [_] [{:name "my_tool"
               :description "Does something"
               :inputSchema {:type "object" :properties {} :required []}
               :handler (fn [_] {:type "text" :text "hello"})}])
  (schema-extensions [_] {})
  (health [_] {:status :ok}))

(defn init-as-addon!
  "Entry point called by the classpath scanner."
  []
  ;; Return result map — the loader handles registration
  {:registered ["acme.my-addon"] :total 1})

Deployment Scenarios

deps.edn dependency

Consumer adds the addon — auto-discovered at startup:

{:deps {io.github.hive-agi/hive-mcp {:git/tag "v0.X.0" :git/sha "..."}
        com.acme/my-addon            {:mvn/version "1.0.0"}}}

Docker / Uberjar

All JARs merged into one. Each addon's META-INF/hive-addons/<id>.edn file has a unique name, so no collisions. Build tools (tools.build, depstar) merge META-INF/ directories correctly.

FROM eclipse-temurin:21-jre
COPY target/hive-mcp-uber.jar /app/hive-mcp.jar
CMD ["java", "-jar", "/app/hive-mcp.jar"]

K8s: Classpath composition

For environments where addons are deployed independently (no uberjar rebuild):

FROM eclipse-temurin:21-jre
COPY deps-jars/ /app/lib/
CMD ["java", "-cp", "/app/lib/*", "clojure.main", "-m", "hive-mcp.server.core"]

Each JAR retains its own META-INF/hive-addons/getResources returns all of them.

K8s: Runtime addon injection

Mount addon JARs without rebuilding the image:

volumes:
  - name: addon-jars
    configMap:
      name: custom-addons
containers:
  - name: hive-mcp
    env:
      - name: JAVA_TOOL_OPTIONS
        value: "-cp /app/lib/*:/addons/*"
    volumeMounts:
      - name: addon-jars
        mountPath: /addons

Source Files

File Purpose
manifest.cljscan-classpath-manifests Classpath scanner (file: + jar: protocols)
manifest.cljinit-from-manifest! Constructor resolution fallback
loader.cljdiscover-addon-manifests Topo-sort + merge with hardcoded list
loader.cljload-extensions! Orchestrates the full 5-step init flow
manifest.cljmanifests-load-order Kahn's algorithm for dependency sort

Backward Compatibility

The hardcoded extension-namespaces list in loader.clj is preserved as a final fallback. Addons discovered via classpath manifest take priority (deduped by init-ns). Removing an addon from the classpath is the only change needed to disable it.

Clone this wiki locally