A Go SDK for MLflow. Supports Experiment Tracking, the Prompt Registry, and the MCP Registry.
- Create, get, update, and delete experiments
- Create, get, update, and delete runs
- Log metrics (single and batch), parameters, and tags
- Search experiments and runs with filter expressions
- Typed run status constants and view type filters
- Load prompts by name (latest or specific version)
- List prompts and versions with filtering and pagination
- Register text prompts and chat prompts (with model configuration)
- Delete prompts, versions, and tags
- Format prompts with variable substitution
- Modify prompts locally with immutable operations
- Register, get, update, delete, and search MCP (Model Context Protocol) servers
- Create, get, update, delete, and search server versions, each with its own
server.json, discovered tools, and per-connection-mode display settings (connect_options) - Create, get, update, search, and delete access endpoints (URLs bound to a server, version, or alias)
- Set/delete tags on servers and versions
- Set/get/delete aliases (e.g. "production") pointing at a version
- Forward custom headers on every request via
WithHeaders - Tenant isolation with
X-MLFLOW-WORKSPACEheader - Compatible with the Red Hat midstream fork (opendatahub-io/mlflow)
- Full context support for cancellation and timeouts
- Structured logging with
slog.Handler - Type-safe error handling
go get github.com/opendatahub-io/mlflow-gopackage main
import (
"context"
"fmt"
"log"
"github.com/opendatahub-io/mlflow-go/mlflow"
)
func main() {
// Create client (reads MLFLOW_TRACKING_URI from environment)
client, err := mlflow.NewClient()
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// Load a prompt
prompt, err := client.PromptRegistry().LoadPrompt(ctx, "my-prompt")
if err != nil {
if mlflow.IsNotFound(err) {
log.Fatal("Prompt not found")
}
log.Fatal(err)
}
fmt.Printf("Loaded: %s v%d\n", prompt.Name, prompt.Version)
fmt.Printf("Template: %s\n", prompt.Template)
}| Variable | Description | Required |
|---|---|---|
MLFLOW_TRACKING_URI |
MLflow server URL | Yes |
MLFLOW_INSECURE_SKIP_TLS_VERIFY |
Allow HTTP (set to true or 1) |
No |
client, err := mlflow.NewClient(
mlflow.WithTrackingURI("https://mlflow.example.com"),
mlflow.WithHeaders(map[string]string{
"Authorization": "Bearer my-token",
}),
mlflow.WithTimeout(60 * time.Second),
)WithHeaders forwards custom HTTP headers on every API request. This is primarily used for workspace-based tenant isolation with the Red Hat midstream fork (opendatahub-io/mlflow), but can also carry additional auth headers or routing metadata.
The midstream fork adds multi-tenant workspace support to MLflow. Each workspace acts as an isolated namespace — prompts, models, and versions created in one workspace are completely invisible from another. The server routes requests to the correct workspace based on the X-MLFLOW-WORKSPACE header.
Note: Workspace isolation is currently available in the Red Hat midstream fork. This feature is planned to be upstreamed to MLflow in the near future.
// Create a client scoped to a workspace
clientA, err := mlflow.NewClient(
mlflow.WithTrackingURI("http://127.0.0.1:5000"),
mlflow.WithInsecure(),
mlflow.WithHeaders(map[string]string{
"X-MLFLOW-WORKSPACE": "team-bella",
}),
)
// Register a prompt in team-bella
clientA.PromptRegistry().RegisterPrompt(ctx, "my-prompt", "Hello {{name}}!")
// Create a second client scoped to team-dora
clientB, err := mlflow.NewClient(
mlflow.WithTrackingURI("http://127.0.0.1:5000"),
mlflow.WithInsecure(),
mlflow.WithHeaders(map[string]string{
"X-MLFLOW-WORKSPACE": "team-dora",
}),
)
// This returns NotFound — the prompt exists only in team-bella
_, err = clientB.PromptRegistry().LoadPrompt(ctx, "my-prompt")
// mlflow.IsNotFound(err) == trueWorkspaces must be pre-created on the server before use. If you reference a workspace that doesn't exist, the server returns a RESOURCE_DOES_NOT_EXIST error. For local development:
# Start midstream server with workspaces enabled
make dev/up-midstream
# Create workspaces
make dev/seed-workspaces// Allow HTTP for local development
client, err := mlflow.NewClient(
mlflow.WithTrackingURI("http://localhost:5000"),
mlflow.WithInsecure(),
)import "github.com/opendatahub-io/mlflow-go/mlflow/tracking"
ctx := context.Background()
// Create an experiment with a kind (controls how the MLflow UI displays it)
expID, err := client.Tracking().CreateExperiment(ctx, "my-experiment",
tracking.WithExperimentKind(tracking.ExperimentKindMLDevelopment),
)
// Create a run in the experiment
run, err := client.Tracking().CreateRun(ctx, expID,
tracking.WithRunName("training-run-1"),
tracking.WithRunTags(map[string]string{"model": "sklearn"}),
)
runID := run.Info.RunID
// Log metrics, params, and tags
err = client.Tracking().LogMetric(ctx, runID, "rmse", 0.85, tracking.WithStep(1))
err = client.Tracking().LogParam(ctx, runID, "learning_rate", "0.01")
err = client.Tracking().SetTag(ctx, runID, "status", "training")
// Mark run as finished
info, err := client.Tracking().UpdateRun(ctx, runID,
tracking.WithStatus(tracking.RunStatusFinished),
tracking.WithEndTime(time.Now()),
)err := client.Tracking().LogBatch(ctx, runID,
[]tracking.Metric{
{Key: "loss", Value: 0.5, Step: 1},
{Key: "loss", Value: 0.3, Step: 2},
{Key: "accuracy", Value: 0.92, Step: 1},
},
[]tracking.Param{
{Key: "optimizer", Value: "adam"},
{Key: "batch_size", Value: "32"},
},
map[string]string{
"framework": "pytorch",
},
)// List experiments (default: active only; use WithExperimentsViewType for deleted/all)
experiments, err := client.Tracking().SearchExperiments(ctx)
for _, e := range experiments.Experiments {
fmt.Printf("[%s] %s (lifecycle: %s)\n", e.ID, e.Name, e.LifecycleStage)
}// Search experiments by name
experiments, err := client.Tracking().SearchExperiments(ctx,
tracking.WithExperimentsFilter("name = 'my-experiment'"),
)
// Search runs with metric filter
runs, err := client.Tracking().SearchRuns(ctx, []string{expID},
tracking.WithRunsFilter("metrics.rmse < 1"),
tracking.WithRunsOrderBy("metrics.rmse ASC"),
tracking.WithRunsMaxResults(10),
)
for _, r := range runs.Runs {
fmt.Printf("Run %s: status=%s\n", r.Info.RunID, r.Info.Status)
}// Get experiment by ID or name
exp, err := client.Tracking().GetExperiment(ctx, expID)
exp, err = client.Tracking().GetExperimentByName(ctx, "my-experiment")
// Get run
run, err := client.Tracking().GetRun(ctx, runID)
// Delete tag, run, experiment
err = client.Tracking().DeleteTag(ctx, runID, "status")
err = client.Tracking().DeleteRun(ctx, runID)
err = client.Tracking().DeleteExperiment(ctx, expID)Upload, list, and download run artifacts via client.Artifacts(). ListArtifacts uses the standard tracking API (GET /api/2.0/mlflow/artifacts/list). For upload and download, local filesystem artifact roots (file:// or path-only) use tracking-server routes directly; otherwise the client tries presigned URLs first (cloud-backed stores), then the mlflow-artifacts proxy when the run uses mlflow-artifacts:/ URIs.
import (
"bytes"
"io"
"log"
"github.com/opendatahub-io/mlflow-go/mlflow/artifacts"
)
// Upload an artifact file
err := client.Artifacts().LogArtifact(ctx, runID, "metrics/output.txt",
bytes.NewReader([]byte("accuracy=0.95")),
artifacts.WithContentType("text/plain"),
)
// List artifacts (optionally filter by path prefix)
list, err := client.Artifacts().ListArtifacts(ctx, runID, artifacts.WithArtifactPath("metrics"))
// Download an artifact file (caller must close the returned reader)
rc, err := client.Artifacts().DownloadArtifact(ctx, runID, "metrics/output.txt")
if err != nil {
log.Fatal(err)
}
defer rc.Close()
data, err := io.ReadAll(rc)
if err != nil {
log.Fatal(err)
}UploadArtifact and DownloadArtifactByPath transfer an object addressed by a storage path directly through the mlflow-artifacts proxy (PUT/GET /api/2.0/mlflow-artifacts/artifacts/{path}), without a run ID. They are meant for large models and datasets: the object is streamed in both directions with no size cap and without buffering the whole thing in memory.
// Upload by storage path (streamed, no size limit; caller keeps ownership of the reader)
err := client.Artifacts().UploadArtifact(ctx, "experiments/1/model.bin",
reader, // any io.Reader (*os.File, *bytes.Reader, ...)
artifacts.WithUploadContentType("application/octet-stream"), // optional
)
// Download by storage path (caller must close the returned reader)
rc, err := client.Artifacts().DownloadArtifactByPath(ctx, "experiments/1/model.bin")
if err != nil {
log.Fatal(err)
}
defer rc.Close()Because these transfers can be arbitrarily large, they are not bounded by the client WithTimeout; bound them with a context deadline. When the context has no deadline, only the wait for response headers is bounded, by mlflow.WithStreamHeaderTimeout (default 5 minutes; a negative value disables the fallback).
For local development, start MLflow with artifact serving enabled (make dev/up configures this automatically):
mlflow server \
--serve-artifacts \
--artifacts-destination ./mlartifactsAlternatively, use direct filesystem mode with --no-serve-artifacts and a filesystem --default-artifact-root; the client uses tracking-server upload/download routes in that case.
Presigned upload requires MLflow 3.12+ with a cloud-backed artifact store (S3, GCS, etc.).
Use typed constants to filter by lifecycle stage:
// Search only deleted experiments
experiments, err := client.Tracking().SearchExperiments(ctx,
tracking.WithExperimentsViewType(tracking.ViewTypeDeletedOnly),
)
// Search all runs (active + deleted)
runs, err := client.Tracking().SearchRuns(ctx, []string{expID},
tracking.WithRunsViewType(tracking.ViewTypeAll),
)Set the experiment kind at creation time to control how the MLflow UI displays the experiment. If not set, the UI will prompt users to select a type.
| Constant | Value | Use case |
|---|---|---|
ExperimentKindMLDevelopment |
custom_model_development |
Traditional ML model training |
ExperimentKindGenAIDevelopment |
genai_development |
GenAI / LLM development |
ExperimentKindFineTuning |
finetuning |
Model fine-tuning |
ExperimentKindForecasting |
forecasting |
Time series forecasting |
ExperimentKindClassification |
classification |
Classification tasks |
ExperimentKindRegression |
regression |
Regression tasks |
ExperimentKindAutoML |
automl |
Automated ML |
expID, err := client.Tracking().CreateExperiment(ctx, "my-genai-experiment",
tracking.WithExperimentKind(tracking.ExperimentKindGenAIDevelopment),
)The SDK uses two main types for prompts:
Prompt– Lightweight metadata returned byListPrompts(). Contains name, description, latest version number, and tags. Does not include template content.PromptVersion– Full prompt content returned byLoadPrompt(). Contains the template (or messages for chat prompts), version number, aliases, model config, and timestamps. This is what you use to format prompts with variables.
Use ListPrompts() to browse available prompts, then LoadPrompt() to fetch the full content when you need it.
import "github.com/opendatahub-io/mlflow-go/mlflow/promptregistry"
prompt, err := client.PromptRegistry().LoadPrompt(ctx, "my-prompt", promptregistry.WithVersion(2))// Load the version pointed to by an alias (e.g., "production", "staging")
prompt, err := client.PromptRegistry().LoadPrompt(ctx, "my-prompt", promptregistry.WithAlias("production"))// Set an alias to point to a specific version
err := client.PromptRegistry().SetPromptAlias(ctx, "my-prompt", "production", 3)
// Delete an alias
err := client.PromptRegistry().DeletePromptAlias(ctx, "my-prompt", "staging")// Delete a prompt (cascades to delete all versions and aliases on MLflow OSS)
err := client.PromptRegistry().DeletePrompt(ctx, "my-prompt")
// Delete a specific version
err = client.PromptRegistry().DeletePromptVersion(ctx, "my-prompt", 2)
if mlflow.IsAliasConflict(err) {
// Databricks only: must remove aliases pointing to this version first
// (MLflow OSS silently removes aliases on version deletion)
_ = client.PromptRegistry().DeletePromptAlias(ctx, "my-prompt", "production")
err = client.PromptRegistry().DeletePromptVersion(ctx, "my-prompt", 2)
}
// Delete tags
err = client.PromptRegistry().DeletePromptTag(ctx, "my-prompt", "environment")
err = client.PromptRegistry().DeletePromptVersionTag(ctx, "my-prompt", 1, "reviewed")list, err := client.PromptRegistry().ListPrompts(ctx)
if err != nil {
log.Fatal(err)
}
for _, info := range list.Prompts {
fmt.Printf("%s (latest: v%d)\n", info.Name, info.LatestVersion)
}
// Pagination: fetch next page if available
if list.NextPageToken != "" {
nextPage, err := client.PromptRegistry().ListPrompts(ctx, promptregistry.WithPageToken(list.NextPageToken))
// ...
}// Filter by name pattern and tags
list, err := client.PromptRegistry().ListPrompts(ctx,
promptregistry.WithNameFilter("dog-%"), // SQL LIKE syntax
promptregistry.WithTagFilter(map[string]string{"category": "pets"}),
promptregistry.WithMaxResults(10),
)versions, err := client.PromptRegistry().ListPromptVersions(ctx, "my-prompt")
if err != nil {
log.Fatal(err)
}
for _, v := range versions.Versions {
fmt.Printf("v%d: %s\n", v.Version, v.CommitMessage)
}
// Limit results
versions, err = client.PromptRegistry().ListPromptVersions(ctx, "my-prompt",
promptregistry.WithVersionsMaxResults(10),
)MLflow OSS Known Issue: MLflow OSS has a bug where the
/model-versions/searchendpoint permanently returns empty results for model versions created in rapid succession (<50ms between calls). The data is written correctly (direct GET by version number works), but the search endpoint never indexes those versions. This affects both SQLite and PostgreSQL backends, pointing to a stale SQLAlchemy session issue in MLflow's multi-worker Uvicorn setup rather than a database-level problem.
ListPromptVersionsworks around this by trying the search endpoint first, and falling back to fetching versions individually (via the@latestalias + direct GET per version) when search returns empty. We plan to report this issue upstream to MLflow.
prompt, err := client.PromptRegistry().RegisterPrompt(ctx, "dog-walker-prompt",
"Time to walk Bella and Dora! Meeting at {{location}} at {{time}}.",
promptregistry.WithCommitMessage("Walk reminder for Bella and Dora"),
promptregistry.WithTags(map[string]string{
"dogs": "bella,dora",
"category": "scheduling",
}),
)
fmt.Printf("Created: %s v%d\n", prompt.Name, prompt.Version)messages := []promptregistry.ChatMessage{
{Role: "system", Content: "You are a helpful dog walking assistant for {{owner}}."},
{Role: "user", Content: "When should I walk {{dog_name}} today?"},
}
// Optional: include model configuration
temp := 0.7
modelConfig := &promptregistry.PromptModelConfig{
Provider: "openai",
ModelName: "gpt-4",
Temperature: &temp,
}
prompt, err := client.PromptRegistry().RegisterChatPrompt(ctx, "dog-assistant",
messages,
promptregistry.WithCommitMessage("Chat assistant for dog walking"),
promptregistry.WithModelConfig(modelConfig),
)
fmt.Printf("Created chat prompt: %s v%d\n", prompt.Name, prompt.Version)// Text prompts - get formatted string directly
prompt, _ := client.PromptRegistry().LoadPrompt(ctx, "dog-walker-prompt")
text, err := prompt.FormatAsText(map[string]string{
"location": "Central Park",
"time": "3pm",
})
// Result: "Time to walk Bella and Dora! Meeting at Central Park at 3pm."
// Chat prompts - get formatted messages
chatPrompt, _ := client.PromptRegistry().LoadPrompt(ctx, "dog-assistant")
messages, err := chatPrompt.FormatAsMessages(map[string]string{
"owner": "Alice",
"dog_name": "Bella",
})
// Returns []ChatMessage with variables substituted
// Generic format - returns a new PromptVersion with variables substituted
formatted, err := prompt.Format(map[string]string{"location": "Park", "time": "3pm"})
// formatted.Template or formatted.Messages contains the result// Load existing prompt
prompt, err := client.PromptRegistry().LoadPrompt(ctx, "dog-walker-prompt")
if err != nil {
log.Fatal(err)
}
// Modify locally (original unchanged)
modified := prompt.
WithTemplate("Hey {{owner}}! Bella and Dora are ready for their walk. Don't forget the treats!").
WithCommitMessage("Added owner and treats reminder")
// Register as new version
newVersion, err := client.PromptRegistry().RegisterPrompt(ctx, modified.Name, modified.Template,
promptregistry.WithCommitMessage(modified.CommitMessage),
)
fmt.Printf("Created version %d\n", newVersion.Version)handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
client, err := mlflow.NewClient(
mlflow.WithLogger(handler),
)The MCP Registry manages Model Context Protocol servers: a centralized catalog for registering, versioning, and sharing MCP servers across a team. Unlike the Prompt Registry and Tracking APIs, the MCP Registry REST endpoints are JSON based rather than protobuf based.
import "github.com/opendatahub-io/mlflow-go/mlflow/mcpregistry"
server, err := client.MCPRegistry().CreateMCPServer(ctx, "com.example/my-server",
mcpregistry.WithServerDescription("Internal tools server"),
)
serverJSON := map[string]any{
"name": "com.example/my-server",
"version": "1.0.0",
"packages": []map[string]any{
{"registryType": "npm", "identifier": "@example/my-mcp-server", "version": "1.0.0"},
},
}
version, err := client.MCPRegistry().CreateMCPServerVersion(ctx, server.Name, serverJSON,
mcpregistry.WithVersionStatus(mcpregistry.MCPServerVersionStatusActive),
mcpregistry.WithVersionConnectOptions(map[string]mcpregistry.ConnectOptionSettings{
"npm": {Hidden: true},
}),
)
fmt.Printf("Created %s v%s with %d tools\n", version.Name, version.Version, len(version.Tools))// Partial update: only fields configured via options are changed
server, err = client.MCPRegistry().UpdateMCPServer(ctx, server.Name,
mcpregistry.WithUpdatedServerDisplayName("My Server"),
)
version, err = client.MCPRegistry().UpdateMCPServerVersion(ctx, server.Name, version.Version,
mcpregistry.WithUpdatedVersionStatus(mcpregistry.MCPServerVersionStatusDeprecated),
)
err = client.MCPRegistry().DeleteMCPServerVersion(ctx, server.Name, version.Version)
err = client.MCPRegistry().DeleteMCPServer(ctx, server.Name)err := client.MCPRegistry().SetMCPServerAlias(ctx, "com.example/my-server", "production", "1.0.0")
version, err := client.MCPRegistry().GetMCPServerVersionByAlias(ctx, "com.example/my-server", "production")
err = client.MCPRegistry().DeleteMCPServerAlias(ctx, "com.example/my-server", "production")servers, err := client.MCPRegistry().SearchMCPServers(ctx,
mcpregistry.WithServersFilter("name LIKE 'com.example%'"),
mcpregistry.WithServersMaxResults(20),
)
versions, err := client.MCPRegistry().SearchMCPServerVersions(ctx, "com.example/my-server")// Bind a reachable URL to the server's "production" alias
endpoint, err := client.MCPRegistry().CreateMCPAccessEndpoint(ctx,
"com.example/my-server", "https://mcp.example.com/my-server",
mcpregistry.WithAccessEndpointServerAlias("production"),
)
// Search endpoints across all servers, or scope to a single server
endpoints, err := client.MCPRegistry().SearchMCPAccessEndpoints(ctx,
mcpregistry.WithAccessEndpointsServerName("com.example/my-server"),
)
// Update the endpoint's URL or pin it to a different version/alias
endpoint, err = client.MCPRegistry().UpdateMCPAccessEndpoint(ctx,
"com.example/my-server", endpoint.ID,
mcpregistry.WithUpdatedEndpointServerAlias("staging"),
)
err = client.MCPRegistry().DeleteMCPAccessEndpoint(ctx, "com.example/my-server", endpoint.ID)err := client.MCPRegistry().SetMCPServerTag(ctx, "com.example/my-server", "team", "platform")
err = client.MCPRegistry().DeleteMCPServerTag(ctx, "com.example/my-server", "team")
// Tags can also be set on a specific version
err = client.MCPRegistry().SetMCPServerVersionTag(ctx, "com.example/my-server", "1.0.0", "stage", "prod")
err = client.MCPRegistry().DeleteMCPServerVersionTag(ctx, "com.example/my-server", "1.0.0", "stage")The SDK provides type-safe error checking:
prompt, err := client.PromptRegistry().LoadPrompt(ctx, "my-prompt")
if err != nil {
switch {
case mlflow.IsNotFound(err):
fmt.Println("Prompt does not exist")
case mlflow.IsUnauthorized(err):
fmt.Println("Invalid token")
case mlflow.IsPermissionDenied(err):
fmt.Println("Access denied")
case mlflow.IsAliasConflict(err):
fmt.Println("Cannot delete: aliases point to this version (Databricks only)")
default:
var apiErr *mlflow.APIError
if errors.As(err, &apiErr) {
fmt.Printf("API error: %s (code: %s)\n",
apiErr.Message, apiErr.Code)
}
}
return
}| Feature | Status |
|---|---|
| Create/get/update/delete experiments | ✅ Supported |
| Create/get/update/delete runs | ✅ Supported |
| Log metrics (single + batch) | ✅ Supported |
| Log params (single + batch) | ✅ Supported |
| Set/delete tags (single + batch) | ✅ Supported |
| Search experiments and runs | ✅ Supported |
| Typed run status and view type | ✅ Supported |
| Experiment kinds (UI classification) | ✅ Supported |
| Set experiment tags | ✅ Supported |
| Restore experiments/runs | ❌ Not yet |
| Metric history | ❌ Not yet |
| Artifact management | ✅ Supported (list, upload, download) |
| Feature | Status |
|---|---|
| Load/register prompts | ✅ Supported |
| Text and chat prompts | ✅ Supported |
Variable substitution ({{var}}) |
✅ Supported |
| Model configuration | ✅ Supported |
| Version and alias management | ✅ Supported |
| List/search with filters | ✅ Supported |
| Tags on registration | ✅ Supported |
| Delete prompts and versions | ✅ Supported |
| Delete tags | ✅ Supported |
Custom headers (WithHeaders) |
✅ Supported |
| Workspace isolation (midstream) | ✅ Supported |
| Set/update tags after creation | ❌ Not yet |
| Update model config after creation | ❌ Not yet |
| Jinja2 templates (conditionals, loops) | ❌ Not yet |
| Response format specification | ❌ Not yet |
| Cache TTL configuration | ❌ Not yet |
| Feature | Status |
|---|---|
| Register/get/update/delete/search servers | ✅ Supported |
| Create/get/update/delete/search server versions | ✅ Supported |
Per-connection-mode display settings (connect_options) |
✅ Supported |
| Create/get/update/search/delete access endpoints | ✅ Supported |
| Set/delete tags on servers and versions | ✅ Supported |
| Alias management (set/get/delete) | ✅ Supported |
- Go 1.23+
- MLflow server (for integration tests)
# Run unit tests
make test/unit
# Run linter
make lint
# Run all checks (lint, vet, tests)
make check
# Start local MLflow server (requires uv)
make dev/up
# Start MLflow from opendatahub-io/mlflow fork (with workspaces enabled)
make dev/up-midstream
# Create test workspaces (team-bella and team-dora)
make dev/seed-workspaces
# Seed sample prompts (featuring Bella and Dora!)
make dev/seed
# Run the sample app
make run-sample
# Run workspace isolation demo (requires midstream server)
make dev/up-midstream # in one terminal
make dev/seed-workspaces
make run-sample-workspaces
# Run sample app against a remote MLflow instance
cp .env.local.example .env.local # fill in credentials
make run-sample-remote
# Run integration tests
make test/integration
# Run integration tests against midstream
make test/integration-ci-midstream
# Stop local MLflow server
make dev/down
# Reset MLflow (nuke data)
make dev/resetmlflow-go/
├── mlflow/ # Public SDK package
│ ├── client.go # Root client with domain accessors
│ ├── options.go # Client-level options
│ ├── errors.go # Error types and helpers
│ ├── tracking/ # Experiment Tracking sub-client
│ │ ├── client.go # Tracking API methods
│ │ ├── types.go # Experiment, Run, Metric, Param types
│ │ └── options.go # Domain-specific options
│ ├── artifacts/ # Artifacts sub-client
│ │ ├── client.go # ListArtifacts, LogArtifact, DownloadArtifact, UploadArtifact, DownloadArtifactByPath
│ │ ├── types.go # FileInfo, ListArtifactsResult types
│ │ └── options.go # Domain-specific options
│ ├── promptregistry/ # Prompt Registry sub-client
│ │ ├── client.go # PromptRegistry API methods
│ │ ├── prompt.go # Prompt, PromptInfo types
│ │ └── options.go # Domain-specific options
│ └── mcpregistry/ # MCP Registry sub-client
│ ├── client.go # MCPRegistry API methods
│ ├── server.go # MCPServer, MCPServerVersion, MCPAccessEndpoint types
│ └── options.go # Domain-specific options
├── internal/ # Internal packages
│ ├── conv/ # Shared type-conversion helpers
│ ├── errors/ # APIError implementation
│ ├── artifact/ # Artifact URI resolution and store logic
│ └── transport/ # HTTP client
├── sample-app/ # Demo application
└── specs/ # Design documentation
Apache 2.0 - see LICENSE