The MCP server builds its own tools against WorkflowService and never touched the
Instance AI adapter, so it carried the same gap: `search_folders` could resolve a
folder name to an id, and then nothing would accept that id.
- `search_workflows` gains `folderId`, `recursive` and `nodeTypes`, and every row
now carries `folder {id, name, path}` in the same `path` shape `search_folders`
returns, so the two are directly comparable.
- A `folderId` the caller cannot read aborts the search with an explanation
rather than returning the unscoped set, which is indistinguishable from the
folder being empty and would be presented as its contents.
- Subtree expansion goes through `findFolderSubtreesForUser`, so the access check
covers the descendants in the same query that finds them.
- `get_workflow_details` reports `folder {id, name}` alongside the existing bare
`parentFolderId`, which required a second lookup to be worth anything.
Folder reads are wired only when folders are licensed, matching how the folder
tools are already registered.
The dependency index already records a row per node type per workflow, kept
current on every save, and the workflow repository already resolves a
`nodeTypes` filter against it. No caller ever supplied one, so "which workflows
use Postgres" meant fetching every workflow and parsing its nodes, and "what is
this folder built out of" was not answerable at all.
- `workflows(list)` gains `nodeTypes`. It matches the node a workflow actually
contains, so a workflow merely NAMED for a service is correctly excluded and
one that uses it under an unrelated name is correctly found.
- `workflows(node-usage)` returns the node-type histogram for a scope, most-used
first, and composes with the folder arguments. Offered only where the adapter
can answer it, so the agent is never shown a summary it would get an error from.
- Counts DISTINCT workflows rather than node instances: three HTTP Request nodes
in one workflow are one user of the type, which is the question being asked.
- The scan is capped, and a capped scan says its counts are a lower bound —
silently truncating would read as a total, and these are numbers a caller
quotes back to the user.
- Gated separately from folder context. The two answer different questions
(where something lives vs what it is made of), and one switch could not tell
which of them moved a result.
Shared field descriptions are hoisted to constants because the schema sanitizer
requires a single description per field name across the action union.
Both adapter suites replace `@n8n/instance-ai` wholesale, so a new export the
adapter reads has to be added by hand or every listing test throws
`No "isFolderContextEnabled" export is defined on the mock`. Imported from the
real module rather than stubbed, so the tests exercise the shipped default.
A case could not previously state that a workflow lives in a folder, so the
nearest expressible thing was "workflows whose names share a prefix" — which is
the wrong signal the agent was already reading, and would have graded green on a
name guess.
- `seed.workflows[].folder` takes a slash path and creates it on restore.
Creation is IDEMPOTENT: unlike workflow names, folder names are not uniquified
per run (a case's prose names the folder the way a user would), so without
reuse five iterations leave five identically named folders and the folder under
test resolves as ambiguous. Folders are not rolled back with the workflows —
they are empty containers, and deleting one another restore just filled is the
more destructive of the two options.
- Four cases under a `folders` tier, each taken from a real thread: a search
workflow reported as missing while it sat in the named folder; a folder
inventory; a folder disambiguating two similarly named workflows; and the scope
of a bulk folder edit.
Each case is built so the wrong heuristic fails rather than accidentally
succeeds — in-folder names share no token with the folder, and a same-named
non-member sits outside it. The disambiguation case grades on a node name unique
to the in-folder workflow (and asserts absence of one unique to the decoy): node
names only reach context once a workflow is opened, which separates opening the
right one from listing both.
A listing could not say which folder a workflow was in, and `list` had no way
to ask for one. The only move available for "the workflows in my X folder" was
a substring guess on workflow NAMES — which both misses members named
differently and sweeps in non-members that share the name.
- `WorkflowSummary.folder` carries `{id, name, path}`. The repository's default
select already joined `parentFolder`; the adapter was discarding it. Paths are
batch-resolved per page through the existing recursive-CTE repository method,
so attribution costs one query for a whole page and nothing when a page has no
foldered workflows.
- `list` accepts `folderPath` / `folderId` / `recursive`. Resolution is staged
and strict — exact path, then exact folder name, then the last segment (a user
writing "personal/raglog" is naming the project, which is not part of a folder
path), then a segment-boundary suffix. It never partially matches.
- An unresolved folder is reported, never silently widened: the result carries
`folderResolution` and a note saying the rows are NOT that folder, listing the
folders that do exist, and refusing the name-filter fallback by name.
- Subtrees are expanded here rather than by the repository. Only the
workflows-and-folders query expands `parentFolderId`; this listing uses the
plain path, where it is an exact match. Expansion walks parent links rather
than path prefixes, because a folder name may itself contain a slash.
- `N8N_INSTANCE_AI_FOLDER_CONTEXT_ENABLED=false` removes the fields from the
advertised schema and suppresses attribution, so both arms of a measurement
run come off one binary.
The integration test runs the real queries against a real database: the folder
scan's column selection has to satisfy the ORDER BY that pagination generates,
and a mocked repository cannot see that constraint.