UluOps Registry MCP README

The full upstream README, mirrored for reference. Install config, tool schemas, adoption signals and our own overview live on the UluOps Registry MCP listing page.

UluOps · The operations layer for agentic work


@uluops/registry-mcp

npm version License: MIT Node.js Version TypeScript Tests

MCP (Model Context Protocol) server for the UluOps Registry API. Provides 44 tools and 4 resources that let Claude Code, OpenCode, Gemini CLI, and other MCP-compatible harnesses browse, create, validate, version, and analyze AI workflow definitions (agents, commands, workflows, pipelines).

Requires Node.js 20.3 or newer. Create an API key at app.uluops.ai/settings/api-keys; API keys begin with ulr_.

Quick start

To check the package without changing your harness configuration, run:

Terminal
npx -y @uluops/registry-mcp --version

To connect the server, add this block to your harness's MCP config (e.g. ~/.claude.json for Claude Code, ~/.config/opencode/opencode.json for OpenCode):

config.json
{
  "mcpServers": {
    "uluops-registry": {
      "command": "npx",
      "args": ["-y", "@uluops/registry-mcp"],
      "env": {
        "ULUOPS_API_KEY": "ulr_your-api-key"
      }
    }
  }
}

Replace the example value with your API key, then restart your harness. To have your harness configuration written for you, use @uluops/setup: npx @uluops/setup.

Configuration

All configuration is passed via environment variables in the env block. No .env file is needed.

VariableDescriptionRequiredDefault
ULUOPS_API_KEYAPI key (ulr_ prefix) from API key settings, or a session tokenYes—
ULUOPS_ORG_SLUGOrg for definition writes: sent as X-Org-Slug on create, update, publish, deprecate, archive, delete, retranslate, upgrade, and fork (the org the fork goes into), where it says which org's definition a name means. Reads and other calls (validate, render, record execution, user lookups) never carry it (since 0.11.0), so the marketplace stays readable; your org's private definitions still appear by membership. A bare name on a read means the org that first published it, so for a name another org published first, a read and a write of it mean different rows — check namespace (now in the compact list output) before writing back what you read.No—
ULUOPS_REGISTRY_URLRegistry API base URL. Because the API key is sent to this URL on every request, only *.uluops.ai and loopback hosts are accepted unless you opt in belowNoproduction registry (loopback when NODE_ENV=development)
ULUOPS_ALLOW_ANY_REGISTRY_HOSTSet to 1 to accept a ULUOPS_REGISTRY_URL host outside *.uluops.ai and loopback (a self-hosted or staging registry). The server warns at startup when it is usedNo—
ULUOPS_ALLOW_PRIVATE_REGISTRYSet to 1 to accept a private-network or cloud-metadata host. Required in addition to the previous variable for those addressesNo—
ULUOPS_REGISTRY_TIMEOUTRequest timeout (ms)No30000
ULUOPS_REGISTRY_RETRIESRetry attemptsNo3
LOG_LEVELLogging level (error, warn, info, debug)Noinfo
ENABLE_FILE_LOGGINGWrite logs to diskNofalse
LOG_DIRLog file directoryNo./logs
VERBOSE_LOGGINGVerbose security decision loggingNofalse
LOG_PERFORMANCE_METRICSLog performance metricsNofalse
WORKSPACE_DIRBase directory for file_path containment (CWE-22 protection). A relative file_path resolves against the server's working directory, not WORKSPACE_DIR, and must still land inside itNocwd
OUTPUT_BASE_DIRBase directory for output_path containment (CWE-22 protection)Nocwd

Design philosophy

Thin client pattern. This MCP server contains zero business logic. All data processing, validation, storage, and rendering are handled by the registry API. The server's sole responsibility is protocol translation between MCP's stdio JSON-RPC and the backend REST API.

Quick examples

Once configured, your harness can invoke these tools through MCP:

jsonc
// Browse published definitions
list_definitions({ "type": "agent", "status": "published", "limit": 10 })

// Get a specific definition with its rendered YAML
get_definition({ type: "agent", name: "code-validator", include_yaml: true })

// Search across all definition types
search_definitions({ query: "validation", type: "agent" })
search_definitions({ query: "code-validator", match: "exact" })
search_definitions({ query: "code-", match: "prefix", page: 2, limit: 10, sort: "name", order: "asc" })

// Validate YAML before publishing (inline or by file path)
validate_definition({ type: "agent", yaml: "..." })
validate_definition({ type: "agent", file_path: "/path/to/agent.yaml" })

// Create and publish a definition
create_definition({ type: "agent", name: "my-agent", yaml: "..." })
publish_definition({ type: "agent", name: "my-agent", version: "1.0.0" })

// One-call create-or-update-and-publish
update_and_publish({ type: "agent", name: "my-agent", version: "1.1.0", yaml: "..." })

// Compare versions in three different formats
diff_versions({ type: "agent", name: "code-validator", from: "1.0.0", to: "1.1.0" })                  // section summary
diff_versions({ type: "agent", name: "code-validator", from: "1.0.0", to: "1.1.0", format: "fields" })  // structural diff + suggested bump
diff_versions({ type: "agent", name: "code-validator", from: "1.0.0", to: "1.1.0", format: "unified", diff_contract: "combined-v1", full: true }) // patch + exact YAML

// Analytics
get_effectiveness({ type: "agent", name: "code-validator", version: "1.1.0" })
compare_effectiveness({ type: "agent", name: "code-validator", versions: ["1.0.0", "1.1.0"] })

Available tools

Core tools (P0)

ToolDescription
list_definitionsList definitions with filters (type, status, domain, visibility, search, name, match, tags, pagination). format: compact (default — type, name, namespace, version, status, visibility, description and optional relevance per item), full (all catalog fields)
get_definitionGet a single definition by type + name, optionally with YAML / runtime / refs
search_definitionsKeyword search by default; match: exact, prefix, or text. Supports page, limit (default 20, max 100), sort and order
list_modelsList AI models with optional filters
resolve_aliasResolve an alias (e.g. sonnet) to provider + modelId
validate_definitionValidate YAML without storing (accepts yaml or file_path)
render_definitionGet rendered markdown for a definition. output_path writes directly to a file

render_definition uses the uluops-full render profile when render_profile is omitted: the full prompt the registry stores. render_profile: "core" is declared-only (registry on definition-factory ≥ 0.81.0): only what the definition declares, with no factory-supplied output contract, scoring instruction or tone. Before that factory, core meant the full prompt minus the UluOps blocks. Inline and file-write results report the effective profile.

Since 0.16.0 (registry-sdk 0.62.0) results also carry the renderer fields:

  • factoryVersion and renderStack ({ factory, nunjucks, yaml });
  • for agents, resolvedProfile, scaffolding and declaredOutputShape.

Pin a core hash on the definition version plus renderStack. scaffolding: false means the render is declared-only.

output_path writes on the MCP server host, within OUTPUT_BASE_DIR (default: server process cwd). Relative paths resolve from that cwd, even when a different root is configured. The root must exist; missing directories below it are created. Paths outside the root and traversal escaping it are refused. Existing symlinks in the file or directories below the root are checked and refused before rendering. Existing files require overwrite: true. Remote clients should omit output_path and consume markdown inline.

File responses retain the inline rendering metadata (target, renderProfile, selectedModel, promptHash, warnings, and any SDK metadata when supplied) alongside success, output_path, and UTF-8 bytes; markdown is written to disk. Warnings and optional metadata have the same presence and values as inline output.

jsonc
// Inline output for remote clients
render_definition({ type: "agent", name: "code-validator", target: "codex" })
// Write beneath the server cwd (or use an absolute path inside OUTPUT_BASE_DIR)
render_definition({ type: "agent", name: "code-validator", target: "codex", output_path: "output/code-validator.toml" })
// Explicit replacement retains rendering diagnostics
render_definition({ type: "agent", name: "code-validator", target: "codex", output_path: "output/code-validator.toml", overwrite: true })

Definition management (P1)

ToolDescription
create_definitionCreate a new draft definition (accepts yaml or file_path)
update_definitionUpdate a draft definition. Smart version-up: if target version is published or doesn't exist and YAML is provided, automatically creates a new draft
publish_definitionPublish a draft definition
deprecate_definitionDeprecate with reason and optional successor
archive_definitionArchive a deprecated definition (terminal lifecycle state)
delete_definitionDelete a definition version, including published, deprecated, or archived versions when no dependents or forks block deletion. A blocked delete returns DELETE_BLOCKED with a recovery action; hidden references are not disclosed

Composite workflows (P1)

ToolDescription
update_and_publishUpdate a draft and publish it in one call. Inherits smart version-up + create fallback from update_definition
batch_publishPublish up to 20 definition versions in one call. Continues on individual failures, returns both published and failed items

Versions & dependencies (P1)

ToolDescription
list_versionsList all versions of a definition
diff_versionsCompare two versions. format: sections (default), fields (structural + suggested bump), unified. Select diff_contract="combined-v1" for an applicable patch; full=true adds exact YAML.
get_dependenciesForward dependency graph
get_dependentsReverse dependency graph
get_execution_statsExecution statistics for a definition version
list_forksList forks of a definition

Forks (P2)

ToolDescription
fork_definitionFork a definition
is_forkableCheck if a definition version can be forked
get_fork_lineageFork ancestry chain

Translation (P2)

ToolDescription
retranslate_definitionRetranslate with the latest translator version
upgrade_definitionUpgrade a definition from legacy format (accepts yaml or file_path)
get_translator_versionGet current translator version

upgrade_definition applies to legacy definitions without storage/translation metadata and creates a new major version. Current-format drafts and already translated definitions return a structured refusal with applicationState: not_applied, a reason, and recovery guidance. Use publish or retranslate when indicated.

If an upgrade response fails validation, the result carries applicationState: unknown: the write may already have committed. Read the definition and list its versions before retrying. Successful responses preserve the previous version and translated artifact metadata. These upgrade semantics require Registry API 0.59.4 or newer.

Lifecycle refusals now carry allowedTransitions; blocked deletion carries blockingResources: { present: true } and a specific recovery action without exposing hidden references. A bad render target lists available targets once. For any write whose SDK response validation fails, applicationState: unknown means read current state before retrying.

Models & languages (P2)

ToolDescription
get_modelGet specific model details by provider + modelId
list_providersList AI providers
list_aliasesList all model aliases
list_languagesList supported definition languages (ADL, CDL, WDL, PDL)
get_languageGet a definition language with its JSON Schema. format: compact (default — condensed digest sufficient to author a definition, ~50–75% smaller), full (complete JSON Schema with patterns, bounds, and examples)

Execution & users (P2)

ToolDescription
record_executionRecord a definition execution (idempotent). Admin-only — executions are recorded automatically by the runtime; user keys get a 403 explaining this
get_userGet public user profile
batch_usersBatch user lookup (max 100); optional format: 'envelope' returns {data,foundIds,missingIds}

Analytics (P3)

ToolDescription
get_effectivenessEffectiveness metrics: voter-weighted quality (one actor, one vote) with the provenance block — independent vs selfReported split, confidence label. Agents are score-only (passRate: null). Pools every version run in the window, whatever version you pass; the description and each response carry a pooled-versions caveat
get_healthHealth grade and issue profile for a definition; the pass-rate factor requires 3+ qualifying actors. Derived from get_effectiveness, so it pools every version too
get_ecosystem_overviewEcosystem-wide analytics overview
get_lineageLineage graph for a definition (versions + forks as a tree)
get_evolutionVersion-over-version metrics with trend detection
get_translation_analyticsVersions grouped by translator version with aggregate metrics
compare_effectivenessCompare effectiveness across 2–5 definition versions side-by-side
get_diff_impactStructural diff combined with metric deltas between two versions

Session

ToolDescription
set_default_typeSet (or clear) a session-level default for the type parameter. When set, definition tools use this type unless explicitly overridden

Available resources

MCP resources provide read-only access to registry data via the registry:// URI scheme.

ResourceURIDescription
Definitionsregistry://definitionsPublished definitions (up to 100)
Modelsregistry://modelsAI model catalog
Definition typesregistry://definition-typesStatic list: agent, command, workflow, pipeline
Providersregistry://providersAI provider list
typescript
read_resource("registry://definitions")
read_resource("registry://models")
read_resource("registry://definition-types")

Rate limiting

This server uses mcp-secure-server with configuration tuned for typical harness usage patterns. Source of truth: src/config/server-options.ts (global), src/config/limits.ts (the shared envelope), src/config/tool-registry.ts (per-tool caps and quotas), and tool-policies.json (per-tool security level and relaxed fields — loaded explicitly since 0.8.2; yaml is not pattern-scanned at this layer, the registry API scans definitions at publish).

SettingValueNotes
Security levelbasic
Max requests/min120Global rate limit
Max message size1 MiB + 64 KiB (1,114,112 bytes)Layer 1 envelope
Max string length1,114,112 charactersPer-string cap. It counts characters, not bytes, and can never bind before the message size
Max param bytes1,114,112 bytesLayer 2 serialized-params cap (requires mcp-secure-server >= 0.0.19-security)
Suspicious message size1,114,112 bytesLayer 3 hard block, not a log-only flag
Burst threshold15Requests within burst window
Burst window5000 ms
Automation detectionDisabledThe calling harness is trusted automation

The four size settings share one value, ENVELOPE_BYTES. Because a string's characters can never outnumber the message's bytes, and the params are part of the message, the message size is the one that binds; the others never fire first. The per-tool maxArgsSize in tool-registry.ts is the tool-scoped gate beneath them (never above; exactly at it for the YAML tools). Each tool's maxEgressBytes is kept ≥ 16 × its maxArgsSize, because mcp-secure-server checks egress at request time as argument bytes × 16.

Why this envelope: it is the registry API's own body limit on definition routes (1 MiB, express.json({ limit: '1mb' })) plus 64 KiB for the JSON-RPC wrapper, so any body the API accepts fits. Until 0.18.0 it was 500 KiB, which a draft create with a CJK YAML at the 153,600-character limit and maximal provenance (50 contributors with CJK names, about 540 KB) did not fit.

Tool schemas mirror the API's field caps (src/config/api-limits.ts), so an over-limit request is refused locally, by field name, instead of after a round trip:

  • yaml at most 153,600 characters on create, update, validate, upgrade and update-and-publish, including YAML read from file_path;
  • on update_and_publish only, yaml also at most 153,600 UTF-8 bytes. The registry API limits YAML in two units: every route counts characters, while validation and publishing also count UTF-8 bytes, so non-ASCII YAML reaches the byte limit first (a CJK YAML over 51,200 characters can be saved as a draft but not published). update_and_publish checks bytes before it writes anything, so it cannot leave a draft whose publish then fails. validate_definition passes the API's valid: false verdict (PAYLOAD_TOO_LARGE) through instead;
  • description at most 1000 chars;
  • tags at most 32, each at most 50 chars;
  • deprecation reason at most 500 chars, successor as name or name@x.y.z;
  • fork new_name at most 200 chars, lowercase letters, digits and hyphens;
  • execution source at most 32 chars, run_id at most 64.

Here "character" means a UTF-16 code unit (JavaScript string length), as the API counts it.

Per-tool quotas are configured in src/config/tool-registry.ts. Read-heavy tools (list, get, search) allow up to 240 req/min. Write tools (create, update, publish) are 30–60 req/min.

Development

bash
git clone git@github.com:Uluops/-uluops-registry-mcp.git
cd -uluops-registry-mcp
npm install
npm run build
npm test
npm run typecheck
npm run lint

License

MIT

Quality metric contracts (F04)

compare_effectiveness and get_diff_impact accept optional quality_contract: "nullable-v1". Select it to preserve absent gate rates/deltas as null with run-weighted basis, gate denominator, and fraction units. Agent gate rates are null. Omission keeps legacy behavior. Requires the F04-capable Registry SDK and producer capability; unsupported selection returns UNSUPPORTED_CONTRACT instead of legacy numbers. Release the tolerant SDK and producer before enabling this option.

Applicable unified diffs

Call diff_versions with format: "unified" and diff_contract: "combined-v1". Add full: true to receive exact sourceYaml/targetYaml alongside unified. An unchanged pair returns an empty patch. The SDK negotiates support and refuses unsupported servers without falling back. Legacy omission keeps full=true precedence (raw fromYaml/toYaml, no patch) until a future major release with at least 90 days notice.

Definition search semantics

list_definitions({ name: 'code-validator' }) defaults to exact identifier matching; match: 'prefix' selects a literal prefix. Identifiers are trimmed and lowercased, with 1–100 printable ASCII characters before trimming; punctuation, quotes, %, _, and backslashes are preserved. name cannot combine with search or match: 'text'. For search_definitions, exact/prefix modes use the required query as the name. Omitting match retains existing keyword behavior; explicit match: 'text' requires a nonblank search/query of 1–100 printable ASCII characters before trimming. New name or explicit match requests negotiate the server's name-v1 capability and refuse unsupported servers without fallback.

Keyword terms of at least three characters after sanitization use FULLTEXT over name, display_name, and description. Short original searches fall back to name/display_name; longer searches whose sanitized terms are shorter than three characters also fall back across description. Tags filter independently by OR-any and are not keyword searched. Actual FULLTEXT rows may include numeric relevance, retained in compact list output. Search responses retain the full list rows. A list request with page and no limit explicitly uses limit 50, matching the API's default. list_definitions and search_definitions cap limit at 100; list_versions and list_providers at 200.

batch_users defaults to the existing map (format: 'map' is also accepted). With format: 'envelope', UUID inputs are normalized to lowercase and deduplicated in request order; both ID arrays use that order and canonical spelling. Unknown, deleted, and null profiles appear in missingIds. The 100-input limit applies before deduplication. The SDK requires consistent producer found/notFound metadata and returns a response-validation error when it is absent or invalid. In envelope format, fields selects public profile fields inside data while retaining user keys and both foundIds and missingIds. For example, fields: ['id'] trims profiles to their IDs; fields: ['data'] keeps complete profiles. Public profile fields remain selectable for all-missing batches, and unknown fields are rejected. Selecting only metadata leaves empty profile objects under their user keys. Default map field selection retains its existing behavior.