Analyst tools
The tools the PostgreSQL MCP node gives an analyst specialist, the node fields that shape them, and the refusals they return. The guide is Analyst mode.
Node fields
Section titled “Node fields”| Field | Input name | Stored as | Notes |
|---|---|---|---|
| Connect Credential | credential |
Credential id | A PostgresUrl credential. It wins over CUTTLELY_POSTGRES_URL. Without either, CUTTLELY_ANALYST=auto uses isolated storage. Which setting wins |
| Available Actions | mcpActions |
JSON array, such as ["query"] |
Without query the node offers no tool. describe comes with query when a selection is set |
| Tables and views | analystRelations |
JSON array of schema-qualified names, or empty | Checkbox list from the listRelations load method. Empty keeps every readable table. Only for a database connected by URL |
| Rules | analystRules |
Text, up to 2,000 characters | Your own instructions. Added to the analyst specialist’s instructions inside <analyst_rules> tags. Cannot widen the selection. A harness save refuses a longer text |
| Row cap | analystRowCap |
Number | Lower than the server row cap only |
| Query timeout (seconds) | analystTimeoutSeconds |
Number | Shorter than the server query timeout only |
listRelations returns one option per table, partitioned table, foreign table, view and materialized view that the login can SELECT, outside pg_catalog, information_schema and other pg_ schemas, partitions left out, at most 2,000. Materialized views come first, then views, then tables, each sorted by schema and name. An option’s name and label are the schema-qualified name with identifiers quoted where Postgres needs it (public.stores, "Sales 2026".daily). Its description is the kind, the column count and the comment, joined with ·. A message, such as Isolated analyst storage or Could not list tables and views, is an option named error.
| Argument | Type | Notes |
|---|---|---|
sql, statement or query |
string | One statement |
cursor |
string | Page cursor from a capped result. Repeats that statement |
page |
integer | The next page, or 1. A stable page number only when the page line names one |
The result is JSON with rows, truncated, and on a capped result summary (the page line) and cursor. Paging is in Paging through large results.
With a selection set, the description ends with Only these relations can be queried. and one line per selected relation that still exists, in the list order:
- <name> (<kind>): <column>, <column>, ... — <comment, cut at 120 characters>Columns are in column order, quoted where Postgres needs it. More than 12 columns list the first 12, then …. A relation with no comment has no — part. A relation with no columns has : <comment> after the kind. Any run of whitespace in a name or comment, newlines included, becomes one space, so each relation stays on one line. Types, column comments, keys and definitions are only in describe. When the database does not answer while the tools are listed, each line is the name alone, and the check still applies to every call.
describe
Section titled “describe”Offered only when Tables and views has at least one name.
| Argument | Type | Notes |
|---|---|---|
name (also relation, table, view) |
string | A selected name, with or without the schema. A bare name must match one relation |
| Field | Present | Value |
|---|---|---|
name |
always | Schema-qualified name |
kind |
always | table, foreign table, view or materialized view |
comment |
when the relation has one | The relation comment |
columns |
always | <column> <type>[ not null][ -- <comment>], in column order |
primaryKey |
when there is one | Column names |
relationships |
when any involve selected names | <from>(<columns>) -> <to>(<columns>) for a foreign key between selected relations, reads <name> for a selected relation a view reads, read by <name> for a selected view that reads it |
definition |
views and materialized views | pg_get_viewdef, cut at 4,000 characters |
values |
when a column qualifies | An object from a quoted column name to its sorted sample values |
describe runs in one transaction with the node’s query timeout. The transaction starts with BEGIN TRANSACTION READ WRITE, even for a role that defaults to read-only transactions, because the sample checks create a temporary view. It turns read only before any sample runs, and it always rolls back. On a read-only standby it starts read only and leaves values out.
values samples only the relation being described, which is a selected one, and only a table, view or materialized view. A column qualifies when its type is text, character varying, character or an enum. The sample is the distinct non-null values among the column’s first 1,000 rows, read with SELECT DISTINCT. Values are the column cast to pg_catalog.text, exactly as stored, whitespace included (a character(n) value loses its trailing pad in that cast). A column is left out when that gives more than 10 values, when any value is longer than 40 characters, or when its sample fails. At most 8 columns are sampled per relation. Each sample statement first passes the same risky-domain and temporary-view checks as query, in a savepoint. A column whose statement does not pass is not sampled, so a risky domain or a login without TEMP leaves values out. The catalog reads and those checks run in a transaction that starts read-write for the temporary view and always rolls back. The transaction then turns read only, and each sample runs in its own savepoint with a 1 second statement timeout, so one timeout skips only that column.
Several analysts on one harness
Section titled “Several analysts on one harness”These apply when a harness roster has more than one specialist with a PostgreSQL MCP binding. With one, the router and Harness Prime text is unchanged.
| What | With several analysts |
|---|---|
| Router and Harness Prime analyst line | ROUTE_ANALYSTS in packages/harness/run.mjs and CHIEF_ANALYSTS in packages/harness/profile.mjs: send to the analyst whose summary and tables fit, to each when several fit, and never answer directly |
| Specialist line in the router and SOUL | Ends with Tables: <selected names>., at most 12 names, then , and <n> more. An analyst with nothing selected has no Tables: part |
| Dialect note | One line per analyst: The <name> analyst database is <PostgreSQL or SQLite>., plus the SQLite id note |
| Router returns no steps for a data task | One more route call with ROUTE_CHOOSE_ANALYST, which replies {"analysts":["NAME"]}. Names that are not analysts are ignored. No usable name, or a failed call, sends the task to every analyst |
show more, next page, page N |
Goes to the analyst named on the latest captured cursor for that chat (latestAnalystCursor returns specialist). An owner no longer on the roster gets ANALYST_PAGE_OWNER_GONE |
Every analyst’s instructions also say to check the distinct values of a column, or use ILIKE (LIKE on SQLite), before reporting zero rows for a text filter.
Refusals
Section titled “Refusals”Each is returned as { "error": "...", "cuttlelyOutcome": "expected" }. An expected result does not pause the analyst tool.
| When | Text starts with |
|---|---|
A selection is set and the statement is not SELECT, WITH, VALUES or TABLE |
This node limits the analyst to selected tables and views, so it runs only SELECT, WITH, VALUES and TABLE queries. |
| The statement reads a relation that is not selected | Cuttlely refused this query: it reads <names>, which this node's table selection does not include. Selected: <names>. |
It calls an operator outside pg_catalog, or a function outside pg_catalog that is not on Allowed functions and fails the user function rule (since 1.4.5, string-body SQL and PL/pgSQL helpers can pass it; for example a VOLATILE helper or one that reads a table or view still fails) |
Cuttlely refused this query: function <schema.name> is not built in. (or operator) |
It uses a type whose input, output, receive or send function is outside pg_catalog |
Cuttlely refused this query: type <name> is not built in. |
It uses an object-identifier type (regclass, regtype, …) |
Cuttlely refused this query: type <name> is not allowed while a table selection is set. |
The database has a domain whose CHECK calls a volatile function, a function or operator outside pg_catalog, a denied built-in, or uses a denied type. Checked before the statement is parsed, for every statement |
Cuttlely refused this query: this database has domains whose CHECK could run other code or change data while a query is checked: <domains>. |
| It calls a built-in that can reach other data or change name resolution | Cuttlely refused this query: <name> is not allowed while a table selection is set. |
The login has no TEMP privilege on the database |
Cuttlely could not check this query against the table selection: this database login has no TEMP privilege on the database |
| The database is a read-only standby (in recovery) | Cuttlely could not check this query: the database is a read-only standby |
| No selected name exists any more | None of the selected tables or views exist any more. |
describe names a relation that is not selected |
<name> is not one of the selected tables or views. Choose one of: |
describe gets a bare name that two selected relations share |
<name> is ambiguous: it matches <names>. Call describe again with the schema-qualified name. |
The built-ins refused while a selection is set are the query_to_xml, table_to_xml, cursor_to_xml, schema_to_xml and database_to_xml families, ts_stat, ts_rewrite, currtid2, loread, set_config, pg_sequence_last_value, pg_input_is_valid, pg_input_error_info, obj_description, col_description, shobj_description, pg_relation_size, pg_total_relation_size, pg_table_size, pg_indexes_size, pg_database_size, pg_tablespace_size, pg_tablespace_location, pg_relation_filenode, pg_relation_filepath, pg_filenode_relation, pg_partition_tree, pg_partition_ancestors, pg_partition_root, pg_relation_is_updatable, pg_column_is_updatable, pg_describe_object, pg_identify_object, pg_identify_object_as_address, pg_terminate_backend, pg_cancel_backend, any name starting with lo_, pg_read_, pg_ls_, pg_stat_file, pg_stat_get_, pg_get_, dblink, has_ or to_reg, and any name ending in _is_visible. The object-identifier types refused are regclass, regproc, regprocedure, regoper, regoperator, regtype, regnamespace, regrole, regcollation, regconfig and regdictionary. A regconfig or regdictionary constant is allowed only when it is passed directly, by oid, to to_tsvector, to_tsquery, plainto_tsquery, phraseto_tsquery, websearch_to_tsquery, ts_headline or ts_lexize. Any other use is refused, including a column, a variable, a nested expression, an input or output coercion, and get_current_ts_config(). The same check reads view definitions, materialized view definitions and row-security policy expressions. Unknown parse-node tags are refused; PostgreSQL 14 names (COALESCE, ROW, CASE, MINMAX, RTE, SETOPERATIONSTMT) and PostgreSQL 16 names (COALESCEEXPR, ROWEXPR, CASEEXPR, CASEWHEN, MINMAXEXPR, RANGETBLENTRY, NULLIFEXPR, BOOLEANTEST) are both accepted. Allow-listed function bodies are trusted by design: listing a function admits that function, while its argument defaults and the functions called inside a SQL-standard body are still checked; a listed string-body function is not read. A user function that is not allow-listed runs only when it is STABLE or IMMUTABLE, not SECURITY DEFINER, has no settings, is written in SQL or PL/pgSQL, is not owned by a superuser, and its body (a SQL-standard node tree, or a string body read as text) does not write, run EXECUTE or read any relation, and calls only functions that pass the same rule. This holds with or without a selection. With no selection, object-identifier types are not refused; a type whose input or output functions are not built in is refused either way. The check itself is How the selection is enforced. The source is packages/harness/runtime/analyst-relations.mjs. scripts/analyst-relations.test.mjs runs against Postgres 14, 15, 16, 17 and 18 in the analyst-postgres CI job. On 14 and 15, pg_input_is_valid and pg_input_error_info do not exist, and a call fails as an unknown function.
Column errors
Section titled “Column errors”With a selection set, a Postgres error for an undefined column (42703, including a JOIN ... USING column one side lacks) or an ambiguous column (42702) ends with Columns of <name>: <columns>. for each selected relation the statement names, in list order. Each list stops at 60 names, then … with no period. A relation counts when the statement names it the way Postgres would resolve it: a qualified name matches on schema and name (hr.stores is not public.stores), a bare name matches the relation’s name, an unquoted name folds to lower case, a quoted name keeps its case ("stores" matches public.stores, "Stores" does not), and text inside a string literal is not a name. A bare name counts for a selected relation in any schema. The error is still an expected result. Other errors are unchanged.