MCP Factory

MCP Factory lets you create custom, domain-specific MCP servers from your base record_types. Instead of generic tools like manage_record, your agents see purpose-built tools like create_invoice, list_payments, and link_invoice_payment.

Why MCP Factory?

Admin MCP (generic)MCP Factory (custom)
Tools13 fixed tools for all operationsOnly the tools your agent needs
Tool namesmanage_recordcreate_invoice, list_payments
Agent needs to knowRekor concepts (record_types, records, upsert)Just the business domain
Use caseSetup and administrationProduction agents doing domain-specific work

Creating a Toolset

Each toolset defines which record_types, operations, relationships, and capabilities to expose. Create toolsets in a preview base, then promote to production.

A toolset is composed from first-class actions. An action is one record_type + one operation; its id is the agent-facing tool name, and it owns the shaping config (typed filters for reads, curated writes and guards for writes). Author the actions first, then compose a toolset that references them by id.

1. Author actions

rekor actions upsert get_invoice     --base my-ws --record_type invoices --operation get
rekor actions upsert search_invoices --base my-ws --record_type invoices --operation list
rekor actions upsert create_payment  --base my-ws --record_type payments --operation create
rekor actions upsert list_payments   --base my-ws --record_type payments --operation list

2. Compose a toolset (shorthand flags)

rekor toolsets upsert invoicing-agent --base my-ws \
  --name "Invoicing Agent" \
  --action get_invoice --action search_invoices \
  --action create_payment --action list_payments \
  --relationship "invoice_payment:create,list" \
  --batch "invoices:create,update" --batch "payments:create" \
  --sql-query

Advanced: per-action shaping + full JSON config

Put per-action shaping (typed filters, curated writes, guards) on the action itself with rekor actions upsert --config; the toolset entry references it and can override the surface name (action_name) or description (description_override):

# Shape the list tool with typed filter params
rekor actions upsert search_invoices --base my-ws --record_type invoices --operation list --config '{
  "description": "Search invoices by customer, status, or date range",
  "filterable_fields": [
    { "field": "status" },
    { "field": "issued_at" },
    { "field": "customer", "match": "text" }
  ]
}'

# Compose the toolset — its actions[] are references, not inline shapes
rekor toolsets upsert invoicing-agent --base my-ws \
  --config '{
    "name": "Invoicing Agent",
    "description": "Tools for managing invoices and payments",
    "actions": [
      { "action": "search_invoices" },
      { "action": "get_invoice" },
      { "action": "create_payment", "description_override": "Record a payment against an invoice" },
      { "action": "list_payments" }
    ],
    "relationships": [
      {
        "rel_type": "invoice_payment",
        "operations": ["create", "list"],
        "names": { "create": "assign_payment" },
        "description_override": "Link a payment to an invoice"
      }
    ],
    "batch": {
      "enabled": true,
      "operations": {
        "invoices": ["create", "update"],
        "payments": ["create"],
        "invoice_payment": ["create"]
      }
    },
    "sql_query": true
  }'

Or load from a file: --config @toolset.json

Connecting an Agent

Get the MCP connection URL, then point any MCP client at it:

rekor toolsets url invoicing-agent
# → https://mcp.rekor.pro/t/invoicing-agent/mcp
{
  "mcpServers": {
    "invoicing": {
      "url": "https://mcp.rekor.pro/t/invoicing-agent/mcp",
      "headers": {
        "Authorization": "Bearer rec_your_token_here"
      }
    }
  }
}

One credential per server: mint a token bound to this toolset in one step with rekor tokens create-for-toolset invoicing-agent --base my-ws (or pass --mint-token to rekor toolsets upsert). Its authorization IS the toolset's tool surface — exactly those record_types and operations, relationships, batch, and SQL only if you enabled it, and nothing else — so a leaked or prompt-injected token can never reach beyond the tools you exposed. Rotate or revoke it independently per agent.

Important: The token must be scoped to exactly one base. Multi-base or wildcard tokens are rejected. (A toolset-bound token satisfies this automatically.)

Which base serves the toolset: the toolset is resolved from the base your token is scoped to. For production, promote the toolset and connect with a token scoped to the production base. To test a not-yet-promoted toolset, connect with a token scoped to the preview base id (my-ws--<preview-slug>). If the toolset can't be resolved for your token's base, initialize returns a clear error telling you to scope to the preview base id or promote — it won't silently hand back a session with zero tools.

Generated Tools

Based on the toolset config, the following tools are generated:

RecordType tools

OperationDefault nameWhat it does
createcreate_{record_type}Create a record — input schema from the record_type JSON Schema, or a curated typed schema scoped to writable_fields when set
getget_{record_type}Get a record by ID
listlist_{record_type}List records — with typed filter parameters when filterable_fields is set, plus a generic filter
updateupdate_{record_type}Update a record (partial data) — restricted to and typed by writable_fields when set
deletedelete_{record_type}Delete a record

Relationship tools

OperationDefault nameWhat it does
createlink_{rel_type}Create a relationship between two records
listlist_{rel_type}List relationships for a record
deleteunlink_{rel_type}Delete a relationship

Other tools

ToolWhen includedWhat it does
batchbatch config presentAtomic multi-operation — only allows configured record_types and operations
sql_querysql_query: trueRead-only SQL queries scoped to the toolset's record_types

Customizing Actions

Each action's id is its agent-facing name — choose it when you author the action (rekor actions upsert search_invoices …). A toolset reference can override two things per surface without touching the tool:

  • action_name — rename this tool in this toolset (e.g. { "action": "search_invoices", "action_name": "find_invoices" }).
  • description_override — replace the tool's default description for this toolset.

Tool names must be unique across the toolset. Relationship tools, declared inline on the toolset, keep a name/names map (name sets the base noun, names overrides a specific operation). The shaping knobs below live on the action — author them with rekor actions upsert <id> --record_type X --operation Y --config '{…}'.

Typed filter parameters

Add filterable_fields to a list action to expose chosen schema fields as typed parameters — so the agent fills native arguments instead of writing a filter expression. Each field becomes a parameter shaped by its type:

  • an enum or boolean field → an exact-match parameter (the agent can only pick a valid value)
  • a number or date field → a range pair ({field}_min/{field}_max, or {field}_after/{field}_before)
  • a text field → a {field}_contains substring parameter
rekor actions upsert search_invoices --base my-ws --record_type invoices --operation list --config '{
  "filterable_fields": [
    { "field": "status" },
    { "field": "issued_at" },
    { "field": "customer", "match": "text" }
  ]
}'

Per field you may set param (rename the generated parameter), match (exact | range | text | any_of | memberany_of accepts a list and matches any; member exposes an array field as a single membership value), an optional enum (constrain the parameter to a fixed set of values so the agent can only pick a valid one), an optional pattern (a regex declaring the valid format of a structured string field — e.g. an id or code shape like ^pat-[0-9]+$ — so a placeholder such as "null"/"undefined" or any wrong format is rejected with an actionable error instead of silently matching nothing; applies to an exact/any_of/member match on a plain string field and can't be combined with enum), and description. An array field auto-exposes as a member parameter; object fields are rejected — expose a nested path (address.city) instead. The generic filter parameter stays on the tool for anything the typed parameters can't express (OR / nesting); typed parameters and the generic filter are combined with AND.

When the typed parameters cover everything an agent needs, set "expose_filter": false on the action to drop the generic filter parameter entirely — keeping the agent-facing tool schema small (lower token cost, less for the agent to reason about). Server-side translation of the typed parameters is unaffected; only the generic escape hatch is removed.

Curated write surface (writable_fields)

The write-side mirror of filterable_fields. Add writable_fields to a create/update action to declare an allowlist of the fields it may set — which does two things at once:

  • Least-privilege, intent-scoped writes. A field the agent sends that isn't on the list is rejected with an actionable error. So a front-desk tool can be barred from ever setting a price or internal field, and one operation can be split into intent tools — a confirm tool allowed to set only status, a reschedule tool allowed to set only the time — instead of relying on prose to fence them.
  • A rich, typed write schema. The tool's data parameter is generated from the record_type schema for exactly the allowed fields — their types, enums, formats, required, and descriptions — instead of a generic any-object slot. The agent gets the same typed, described, validated guidance on writes that filterable_fields gives on reads.
rekor actions upsert reschedule_appointment --base my-ws --record_type appointments --operation update --config '{
  "writable_fields": [
    { "field": "start_time", "param": "when", "description": "New start time (ISO 8601)" }
  ]
}'

Per field you may set param (rename the key the agent sets in data — the tool maps it back to the real field) and description (override the field's description). Fields are top-level only (writes merge at the top level — a partial update changes just the fields you send and leaves the rest untouched; nested objects are replaced whole). The record_type schema stays the validation source of truth — writable_fields shapes which fields the tool exposes and how they're described, it doesn't re-validate values. A create action's allowlist must cover every required field (else it could never satisfy the schema) — this is checked when the action is saved and at promotion. Omit writable_fields to keep the generic any-field data slot.

Selecting a named write binding (binding)

When a proxy-backed record_type's source declares named write bindings — a create/update/delete operation given as a map of named endpoint variants (see External Sources) — a write action picks which variant it dispatches to with a binding. This is what lets one canonical record_type serve a backend whose writes fan out to several endpoints: author two create actions with distinct ids (book_trial, book_makeup) that select different bindings of the same source, then reference both from the toolset.

rekor actions upsert book_trial  --base my-ws --record_type bookings --operation create --config '{ "binding": "trial" }'
rekor actions upsert book_makeup --base my-ws --record_type bookings --operation create --config '{ "binding": "makeup" }'

The agent never sees a binding parameter — it's injected server-side, exactly like a precondition. A binding selector is required when the bound operation is a binding map with no default, and rejected when the operation is a single endpoint or names a binding the source doesn't declare — validated when the toolset is saved and re-checked at promotion.

Hiding the machinery parameters

A generated list tool also carries sort, limit, offset, and fields. Each can be hidden from the agent — "expose_sort"/"expose_limit"/"expose_offset"/"expose_fields": false — and given a server-side default ("default_sort"/"default_limit"/"default_fields") that still applies when the parameter is hidden. "default_fields" takes the same shape as the fields parameter — a comma-separated string ("external_id,data.name") or an array (["external_id", "data.name"]). A hidden limit surfaces a "truncated" flag in the response if it caps the result, so rows are never silently dropped.

"agent_minimal": true is a convenience preset: it hides every machinery parameter (including filter) and applies a generous default limit, leaving an inputSchema that is pure typed semantics — the job-to-be-done expressed as parameters. Any explicit expose_*/default_* on the same action overrides the preset.

Batch-Only Patterns

You can expose operations only through batch, not as standalone tools. For example, invoices that must always be created atomically with their line items — reference read-only tools, and put the write ops in batch only (author get_invoice/list_invoices/get_line_item/list_line_items first):

{
  "actions": [
    { "action": "get_invoice" },
    { "action": "list_invoices" },
    { "action": "get_line_item" },
    { "action": "list_line_items" }
  ],
  "batch": {
    "enabled": true,
    "operations": {
      "invoices": ["create", "update"],
      "line_items": ["create"],
      "invoice_line_item": ["create"]
    }
  }
}

No standalone create_invoices — the agent must use the batch tool, ensuring invoices and line items are always created atomically.

Managing Toolsets

# List all toolsets
rekor toolsets list --base my-ws

# Get toolset details
rekor toolsets get invoicing-agent --base my-ws

# Get with resolved record_type schemas
rekor toolsets get invoicing-agent --base my-ws --resolved

# Delete a toolset
rekor toolsets delete invoicing-agent --base my-ws

Toolsets are base config — they can only be created or modified in preview bases. Promote to production when ready, just like record_types, inbound webhooks, and triggers.

MCP Factory — Rekor