feat(mcp): search every schema from search_schema when no schema is named - #3097
Merged
Merged
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #3060 (#3048).
The gap
search_schemalisted one schema: the one named, or the one the connection was browsing. On a PostgreSQL database browsingpublic, an AI client asking where thetimesheettable is got"matches": []forattendance.timesheet. That is the gap #3048 reported for Open Quickly and the sidebar filter, which #3060 closed for the UI only.What changed
schemaargument, per-schema engine (PostgreSQL family, DuckDB, SQL Server, SurrealDB, and the hierarchical engines such as Oracle, Snowflake, Trino, BigQuery): tables, views and other table-like objects come from every schema throughCatalogTableListing, the listing feat(sidebar): find tables in every schema from Open Quickly and the sidebar filter #3060 built, less the engine's system schemas (pg_catalog,information_schema,pg_toaston PostgreSQL). PostgreSQL answers in one query (fetchTablesInAllSchemas); the others are listed schema by schema through the one metadata scope, the owner's own fallback. No new per-schema loop.schemanamed: that schema alone, columns included, as before..flat/.byDatabaseengines): unchanged, the whole database.schema(nullon an engine without schemas). Column matches never did; their schema is the one the driver actually read (currentSchema), not the requested one.is_truncatedwastruewhenever the matches reached the limit exactly. It is now set only when more matches existed.database,schema(the one named, elsenull),unlisted_schemas,column_search,columns_schema.Object kinds
schemaschemaobject_typeaslist_tablesreports itcolumns_schemalist_routines,list_types,list_triggerscover them per schemaColumns stay in one schema because a column read of every schema is a whole-catalog read per search: a per-schema column loop is what this change is not allowed to add, and no driver has a one-call all-schema column read. The result says which schema the columns came from, so a client narrows to another by naming it.
Why the listing is read fresh, not from the sidebar's copy
DatabaseTreeMetadataServiceholds the listing stale-while-revalidate, marked stale only by catalog changes the app itself sees. A table created by a migration the agent ran in a terminal would stay invisible to the tool until a reconnect. The tree service also swallows errors into a string, while this tool has to returnnot_connectedand the driver's own error, and an MCP call naming another database would populate and redraw sidebar state for a database the user never opened. So the tool callsCatalogTableListing.tables(in:excludingSchemas:metadata:)throughDatabaseManager.withMetadataDriverwith the scope it already resolved: one query per call on PostgreSQL, the same cost the oldfetchTableshad.Policy and partial results
ToolsCallHandler: token scopetools:read, connection allowlist, AI policy Never, External Clients Blocked, and the approval prompt. The search reads the catalog of the one connection and database it resolved, never another connection. Nothing reaches the statement gate, so read-only and Safe Mode are not involved, as before.termnon-empty,limit1 to 500 (default 50),schemaanddatabasestrings. "Was a schema named" is one decode,MCPScopeArguments.namedSchema, which scope resolution now uses too, so a blankschemameans not named for both.unlisted_schemasrather than reading as "no match there". A lost connection, or every schema failing, fails the call, asCatalogTableListingalready decides, and a connection lost during the column read fails it too.try?). It still does not fail the call, since the table matches are worth returning, butcolumn_searchnow saysfailed. When table matches alone exceed the limit, columns are not read andcolumn_searchsayslimit_reached.publicLogShape), never schema names, the term or the message.Tests
MCPSchemaSearchTestsandMCPSchemaSearchPayloadTests(new, Swift Testing, 19 cases) drivesMCPSchemaSearch.runthrough a recordingScopedMetadataProvidingoverMockDatabaseDriver, and the payload encoder directly. The MCP tool harness uses a liveMCPConnectionBridgewith no driver seam, so the argument checks stay inMCPCatalogToolTests.A search that names no schema finds a table in another schemais the regression test. Its fixture answersfetchTables(schema: "public")withusersalone, which is the read the old code made, so pointing the table read back atfetchTables(schema: scope.schema)turns it red.Naming a schema searches that schema aloneruns exactly that old read on the same fixture and asserts it finds nothing, which is the before half on record. No separate mutation run was made.schema, and the output schema.Verdicts:
verify.sh build: PASS.verify.sh test MCPSchemaSearchTests MCPSchemaSearchPayloadTests MCPEnumeratedArgumentTests CatalogTableListingTests ListTablesToolTests SQLSchemaProviderTests: PASS, 74 executed, 74 passed, 19 of them in the new suites. An earlier run reported an Xcode build-service crash (environment), and the rerun passed.verify.sh linton the six changed Swift files: 0 violations.verify.sh docs: PASS.origin/main. The rebase conflicted only inCHANGELOG.md, and none of the files this change depends on moved.libsqlite3_vendored.a, because the published libs archive would not finish downloading here. That lib only affects the SQLite plugin, which this change does not touch.Reviews
list_schemasandlist_tablesalready expose (system schemas now excluded), that it returns metadata and no row data, thattermnever reaches SQL, that authorization inToolsCallHandleris untouched, and that the new log line carries the error shape only.column_search: failed. It now fails the call, matchingCatalogTableListing.currentSchema, so an engine without schemas reportsnull.column_searchvocabulary is now aCaseIterableenum rather than a hand-kept list.schemaargument is decoded once for both the scope and the search.is_truncatedguess, and a wrongtruecosts the client a second call.CatalogTableListing's own fallback, queued per schema on the metadata lane. Reusing the sidebar's cached listing is the stale-data trade-off described above.MetadataConnectionPool.acquireEntry,Key(scope:workload:)), so a scope without a schema never shares a driver that was switched.Docs:
docs/external-api/mcp-tools.mdx(table row, a newsearch_schemasection, the defaults note) anddocs/external-api/mcp-resources.mdx.