# Addon Classpath Discovery **Status**: Implemented (2026-02-13) **Implements**: [ADR-0007](ADR-0007-hive-addons-architecture) ## 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 ```edn ;; 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 ```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 ```clojure (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: ```edn {: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/.edn` file has a unique name, so no collisions. Build tools (`tools.build`, `depstar`) merge `META-INF/` directories correctly. ```dockerfile 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): ```dockerfile 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: ```yaml 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.clj` — `scan-classpath-manifests` | Classpath scanner (file: + jar: protocols) | | `manifest.clj` — `init-from-manifest!` | Constructor resolution fallback | | `loader.clj` — `discover-addon-manifests` | Topo-sort + merge with hardcoded list | | `loader.clj` — `load-extensions!` | Orchestrates the full 5-step init flow | | `manifest.clj` — `manifests-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.