-
Notifications
You must be signed in to change notification settings - Fork 3
Addon Classpath Discovery
Status: Implemented (2026-02-13) Implements: ADR-0007
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.
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
my-addon/
deps.edn
src/my_addon/init.clj
resources/
META-INF/
hive-addons/
my-addon.edn <- manifest (auto-discovered)
;; 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} expansionRequired fields: :addon/id, :addon/type, :addon/init-ns, :addon/init-fn
{:paths ["src" "resources"] ;; "resources" MUST be in :paths
:deps {io.github.hive-agi/hive-mcp {:git/tag "v0.X.0" :git/sha "..."}}}(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})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"}}}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"]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.
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| 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 |
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.