UluOps · The operations layer for agentic work
@uluops/registry-mcp
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:
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):
{
"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.
| Variable | Description | Required | Default |
|---|---|---|---|
ULUOPS_API_KEY | API key (ulr_ prefix) from API key settings, or a session token | Yes | — |
ULUOPS_ORG_SLUG | Org 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_URL | Registry 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 below | No | production registry (loopback when NODE_ENV=development) |
ULUOPS_ALLOW_ANY_REGISTRY_HOST | Set 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 used | No | — |
ULUOPS_ALLOW_PRIVATE_REGISTRY | Set to 1 to accept a private-network or cloud-metadata host. Required in addition to the previous variable for those addresses | No | — |
ULUOPS_REGISTRY_TIMEOUT | Request timeout (ms) | No | 30000 |
ULUOPS_REGISTRY_RETRIES | Retry attempts | No | 3 |
LOG_LEVEL | Logging level (error, warn, info, debug) | No | info |
ENABLE_FILE_LOGGING | Write logs to disk | No | false |
LOG_DIR | Log file directory | No | ./logs |
VERBOSE_LOGGING | Verbose security decision logging | No | false |
LOG_PERFORMANCE_METRICS | Log performance metrics | No | false |
WORKSPACE_DIR | Base 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 it | No | cwd |
OUTPUT_BASE_DIR | Base directory for output_path containment (CWE-22 protection) | No | cwd |
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:
// 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)
| Tool | Description |
|---|---|
list_definitions | List 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_definition | Get a single definition by type + name, optionally with YAML / runtime / refs |
search_definitions | Keyword search by default; match: exact, prefix, or text. Supports page, limit (default 20, max 100), sort and order |
list_models | List AI models with optional filters |
resolve_alias | Resolve an alias (e.g. sonnet) to provider + modelId |
validate_definition | Validate YAML without storing (accepts yaml or file_path) |
render_definition | Get 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:
factoryVersionandrenderStack({ factory, nunjucks, yaml });- for agents,
resolvedProfile,scaffoldinganddeclaredOutputShape.
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.
// 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)
| Tool | Description |
|---|---|
create_definition | Create a new draft definition (accepts yaml or file_path) |
update_definition | Update 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_definition | Publish a draft definition |
deprecate_definition | Deprecate with reason and optional successor |
archive_definition | Archive a deprecated definition (terminal lifecycle state) |
delete_definition | Delete 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)
| Tool | Description |
|---|---|
update_and_publish | Update a draft and publish it in one call. Inherits smart version-up + create fallback from update_definition |
batch_publish | Publish up to 20 definition versions in one call. Continues on individual failures, returns both published and failed items |
Versions & dependencies (P1)
| Tool | Description |
|---|---|
list_versions | List all versions of a definition |
diff_versions | Compare 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_dependencies | Forward dependency graph |
get_dependents | Reverse dependency graph |
get_execution_stats | Execution statistics for a definition version |
list_forks | List forks of a definition |
Forks (P2)
| Tool | Description |
|---|---|
fork_definition | Fork a definition |
is_forkable | Check if a definition version can be forked |
get_fork_lineage | Fork ancestry chain |
Translation (P2)
| Tool | Description |
|---|---|
retranslate_definition | Retranslate with the latest translator version |
upgrade_definition | Upgrade a definition from legacy format (accepts yaml or file_path) |
get_translator_version | Get 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)
| Tool | Description |
|---|---|
get_model | Get specific model details by provider + modelId |
list_providers | List AI providers |
list_aliases | List all model aliases |
list_languages | List supported definition languages (ADL, CDL, WDL, PDL) |
get_language | Get 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)
| Tool | Description |
|---|---|
record_execution | Record a definition execution (idempotent). Admin-only — executions are recorded automatically by the runtime; user keys get a 403 explaining this |
get_user | Get public user profile |
batch_users | Batch user lookup (max 100); optional format: 'envelope' returns {data,foundIds,missingIds} |
Analytics (P3)
| Tool | Description |
|---|---|
get_effectiveness | Effectiveness 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_health | Health 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_overview | Ecosystem-wide analytics overview |
get_lineage | Lineage graph for a definition (versions + forks as a tree) |
get_evolution | Version-over-version metrics with trend detection |
get_translation_analytics | Versions grouped by translator version with aggregate metrics |
compare_effectiveness | Compare effectiveness across 2–5 definition versions side-by-side |
get_diff_impact | Structural diff combined with metric deltas between two versions |
Session
| Tool | Description |
|---|---|
set_default_type | Set (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.
| Resource | URI | Description |
|---|---|---|
| Definitions | registry://definitions | Published definitions (up to 100) |
| Models | registry://models | AI model catalog |
| Definition types | registry://definition-types | Static list: agent, command, workflow, pipeline |
| Providers | registry://providers | AI provider list |
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).
| Setting | Value | Notes |
|---|---|---|
| Security level | basic | |
| Max requests/min | 120 | Global rate limit |
| Max message size | 1 MiB + 64 KiB (1,114,112 bytes) | Layer 1 envelope |
| Max string length | 1,114,112 characters | Per-string cap. It counts characters, not bytes, and can never bind before the message size |
| Max param bytes | 1,114,112 bytes | Layer 2 serialized-params cap (requires mcp-secure-server >= 0.0.19-security) |
| Suspicious message size | 1,114,112 bytes | Layer 3 hard block, not a log-only flag |
| Burst threshold | 15 | Requests within burst window |
| Burst window | 5000 ms | |
| Automation detection | Disabled | The 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:
yamlat most 153,600 characters on create, update, validate, upgrade and update-and-publish, including YAML read fromfile_path;- on
update_and_publishonly,yamlalso 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_publishchecks bytes before it writes anything, so it cannot leave a draft whose publish then fails.validate_definitionpasses the API'svalid: falseverdict (PAYLOAD_TOO_LARGE) through instead; descriptionat most 1000 chars;tagsat most 32, each at most 50 chars;- deprecation
reasonat most 500 chars,successorasnameorname@x.y.z; - fork
new_nameat most 200 chars, lowercase letters, digits and hyphens; - execution
sourceat most 32 chars,run_idat 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
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.