Power tools — the generic engine
Most of the time you'll use a product's named tools — create_account,
search_accounts, summarize_account — on its own endpoint. Underneath those sits
a generic engine: a low-level "do anything" layer that works against any
table in your active product's live schema. It's the fallback when there isn't a
named tool for what you want — ad-hoc inspection, reporting across arbitrary tables,
bulk reads, or relating records that the named tools don't connect for you.
💬 Just ask
- "Pull every account created last month into a table for me."
- "Link this contact to that support ticket."
You don't call these tools yourself — just tell your assistant; the technical reference below is for when you want the details.
The generic engine lives on the concierge endpoint https://hdls.ai/api/mcp — the
same control plane you use to install products and invite teammates,
not a product endpoint. (Product endpoints at /api/mcp/<slug> deliberately expose
only their named tools; the generic engine lives only on the concierge.) The
active product for a request is the schema your credential is pointed at; switch
it for a single request with the x-hdls-product: <slug> header.
Discover, then act. The engine is schema-driven. The pattern is always
list_tables→describe_table→ read/write. Every identifier you name is validated against the live schema, every value is parameterized, and every query is tenant-scoped — you never pass atenant_id.
At a glance
| Stage | Tools | Minimum role |
|---|---|---|
| Discover | list_tables, describe_table | reader |
| Read & analyse | query_records, get_record, aggregate_records | reader |
| Search | semantic_search, hybrid_search | reader |
| Relate | list_links, traverse_link, list_relations, query_related | reader |
| Write | insert_record, update_record, delete_record | member |
| Link | create_link, delete_link | member |
Discover: list_tables & describe_table
Start by seeing what's there.
// tool: list_tables
{}
Then inspect a table's shape — its columns, types, primary key, foreign keys, and the grants that decide whether you can write to it:
// tool: describe_table
{ "table": "deal" }
You read these first so you know the real column names before you query or write — the engine rejects any column it didn't discover.
Read & analyse
query_records — filter, search, sort, paginate
The workhorse read tool. Filters accept either a simple equality map or a list of operator conditions, all AND-ed:
// tool: query_records
{
"table": "deal",
"filters": [
{ "column": "stage", "op": "neq", "value": "lost" },
{ "column": "amount", "op": "gte", "value": 10000 }
],
"orderBy": { "column": "amount", "dir": "desc" },
"limit": 25
}
The available operators are eq, neq, gt, gte, lt, lte, like, ilike,
in (value is an array), between (value is [lo, hi]), is_null, and
is_not_null. A free-text search string matches across the table's text columns.
get_record — one row by id
// tool: get_record
{ "table": "account", "id": "acc_01H…" }
aggregate_records — group-by / pivot reporting
Run a SQL GROUP BY (or a spreadsheet-style pivot) safely. Give it groupBy
columns and one or more metrics (count, sum, avg, min, max); count
may omit its column for COUNT(*). You can filter rows, filter groups with
HAVING on a metric alias, order by a group column or metric alias, and limit.
// tool: aggregate_records
{
"table": "deal",
"groupBy": ["stage"],
"metrics": [
{ "fn": "count", "alias": "deals" },
{ "fn": "sum", "column": "amount", "alias": "pipeline_value" }
],
"orderBy": { "column": "pipeline_value", "dir": "desc" }
}
You get one row per group with the computed metrics — your pipeline by stage, tickets by severity, revenue by month, all without writing SQL.
Search: semantic & hybrid
For tables that carry an embedding column (most product entities do — e.g. CRM
contacts and activities), the engine offers meaning-based search.
semantic_search — nearest by meaning
// tool: semantic_search
{ "table": "contact", "query": "VP of engineering interested in security", "k": 10 }
Embeds your query and returns the top-k nearest rows by cosine distance (lower = closer). Rows not yet embedded are skipped.
hybrid_search — meaning + keywords, fused
// tool: hybrid_search
{ "table": "activity", "query": "renewal risk pricing pushback", "k": 10 }
Fuses the semantic (vector) results with full-text/keyword results using Reciprocal Rank Fusion for higher precision than either alone — each hit comes back with a fused score and its rank in both lists.
Relate: links across products
This is the engine's most distinctive capability. Records in different products can be associated and followed — a CRM contact linked to a support ticket, a deal linked to a contract — without any hard-wired cross-product schema.
create_link — relate two records (member+)
// tool: create_link
{
"fromTable": "account",
"fromId": "acc_01H…",
"toSchema": "support",
"toTable": "ticket",
"toId": "tkt_01H…",
"relation": "ticket_of"
}
Both sides are validated against the live schema before the link is stored; the link is scoped to your workspace.
traverse_link — follow the links and fetch the records
// tool: traverse_link
{ "table": "account", "id": "acc_01H…", "relation": "ticket_of" }
Follows the links from a record and fetches each linked record — even when it
lives in another product's schema — returning [{ link, record }]. Cross-product
reads stay tenant-scoped: you only ever reach records your workspace is allowed to
see, and only along links it owns.
list_links, list_relations, query_related
list_linksshows every link a record participates in (both directions).list_relationsshows how a table connects via real foreign keys (and links).query_relatedwalks a foreign-key relationship from a base row to its related rows.
Write: insert_record, update_record, delete_record (member+)
// tool: insert_record
{ "table": "account", "values": { "name": "Acme Corp", "domain": "acme.com" } }
// tool: update_record
{ "table": "account", "id": "acc_01H…", "values": { "industry": "Manufacturing" } }
// tool: delete_record
{ "table": "ticket", "id": "tkt_01H…" }
On managed workspaces, tenant_id (and any record owner) is stamped server-side
— never supplied by you. Append-only tables reject update and delete by design.
When to reach for the generic engine
- There's no named tool for it. A product gives you purpose-built tools for its common jobs; the generic engine covers everything else against any table.
- You're reporting or inspecting across tables.
aggregate_recordsandquery_recordsare the fastest path to an ad-hoc report. - You're relating records across products.
create_link/traverse_linkare the only way to associate records in different products.
Prefer named tools when one exists. A product's own endpoint (
/api/mcp/<slug>) exposes tools shaped exactly for that product's workflow. Reach for the generic engine when you need to go off that paved path — it's the universal layer beneath everything. New to the two-endpoint model? See What hdls is & how it works.
Where to go next
- See where the engine fits among the named product tools: Products.
- Point the engine at your own database: Bring your own database.
- Let your own customers use a narrow, isolated slice: Customer portals.
- Review how tenant isolation, RLS, and OAuth keep every query scoped: Security & trust.