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) | |
|---|---|---|
| Tools | 13 fixed tools for all operations | Only the tools your agent needs |
| Tool names | manage_record | create_invoice, list_payments |
| Agent needs to know | Rekor concepts (record_types, records, upsert) | Just the business domain |
| Use case | Setup and administration | Production 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
| Operation | Default name | What it does |
|---|---|---|
create | create_{record_type} | Create a record — input schema from the record_type JSON Schema, or a curated typed schema scoped to writable_fields when set |
get | get_{record_type} | Get a record by ID |
list | list_{record_type} | List records — with typed filter parameters when filterable_fields is set, plus a generic filter |
update | update_{record_type} | Update a record (partial data) — restricted to and typed by writable_fields when set |
delete | delete_{record_type} | Delete a record |
Relationship tools
| Operation | Default name | What it does |
|---|---|---|
create | link_{rel_type} | Create a relationship between two records |
list | list_{rel_type} | List relationships for a record |
delete | unlink_{rel_type} | Delete a relationship |
Other tools
| Tool | When included | What it does |
|---|---|---|
batch | batch config present | Atomic multi-operation — only allows configured record_types and operations |
sql_query | sql_query: true | Read-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}_containssubstring 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 | member — any_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
confirmtool allowed to set onlystatus, arescheduletool allowed to set only the time — instead of relying on prose to fence them. - A rich, typed write schema. The tool's
dataparameter 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 thatfilterable_fieldsgives 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.