**[UluOps](https://uluops.ai)** · The operations layer for agentic work --- # @uluops/registry-mcp [![npm version](https://img.shields.io/npm/v/@uluops/registry-mcp.svg)](https://www.npmjs.com/package/@uluops/registry-mcp) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Node.js Version](https://img.shields.io/node/v/@uluops/registry-mcp)](https://nodejs.org) [![TypeScript](https://img.shields.io/badge/TypeScript-5.7+-blue.svg)](https://www.typescriptlang.org/) [![Tests](https://img.shields.io/badge/tests-passing-brightgreen)](src/__tests__/) MCP (Model Context Protocol) server for the [UluOps](https://uluops.ai) 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](https://app.uluops.ai/settings/api-keys); API keys begin with `ulr_`. ## Quick start To check the package without changing your harness configuration, run: ```bash 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): ```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](https://www.npmjs.com/package/@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](https://app.uluops.ai/settings/api-keys), 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: ```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) | 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: - `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) | 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 | ```typescript read_resource("registry://definitions") read_resource("registry://models") read_resource("registry://definition-types") ``` ## Rate limiting This server uses [mcp-secure-server](https://github.com/aself101/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: - `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.