Skip to content

Commit abcae6b

Browse files
tianzhouclaude
andauthored
fix: scope view search to the default schema (#364)
searchViews called connector.getSchemas() directly instead of resolveDefaultSchemas(), unlike the tables/columns/procedures/indexes paths. For MySQL/MariaDB, getSchemas() enumerates every user database on the server, so `search_objects` with object_type "view" and no schema filter listed views across all databases on the host even when the DSN named a specific one — contradicting the anti-leak intent documented on resolveDefaultSchemas. Connectors that report no default schema (e.g. a MySQL DSN with no database name) still fall back to the full list, so db-less connections are unaffected. Also corrects CLAUDE.md, which documented a `source_id` tool parameter that does not exist. Multi-database routing is by tool name (execute_sql_{source_id}); source_id appears in tool output metadata only. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6fce26c commit abcae6b

3 files changed

Lines changed: 42 additions & 3 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Key architectural patterns:
5353
- Runs in stateless mode (no SSE support) - GET requests to `/mcp` return 405 Method Not Allowed
5454
- Tests in `src/__tests__/json-rpc-integration.test.ts`
5555
- **Tool Handlers**: Clean separation of MCP protocol concerns
56-
- Tools accept optional `source_id` parameter for multi-database routing
56+
- Multi-database routing is by tool name, not by parameter: one tool instance is registered per source, named `execute_sql_{source_id}` / `search_objects_{source_id}` (single-source configs keep the bare `execute_sql` / `search_objects` names). See `src/tools/index.ts` and `src/utils/tool-metadata.ts`. `source_id` appears in tool *output* metadata only.
5757
- **Token-Efficient Schema Exploration**: Unified search/list tool with progressive disclosure
5858
- `search_objects`: Single tool for both pattern-based search and listing all objects
5959
- Pattern parameter defaults to `%` (match all) - optional for listing use cases
@@ -107,7 +107,7 @@ DBHub supports three configuration methods (in priority order):
107107
- Path expansion for `~/` in file paths
108108
- Automatic password redaction in logs
109109
- First source is the default database
110-
- Usage in MCP tools: Add optional `source_id` parameter (e.g., `execute_sql(sql, source_id="prod_pg")`)
110+
- Usage in MCP tools: each source gets its own tool, suffixed with the normalized source id (e.g. `execute_sql_prod_pg(sql)`, `search_objects_staging_mysql(...)`)
111111
- See `dbhub.toml.example` for complete configuration reference
112112
- Documentation: https://dbhub.ai/config/toml
113113

src/tools/__tests__/search-objects.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,6 +1351,11 @@ describe('search_database_objects tool', () => {
13511351
if (schema === 'other_db') return ['secrets'];
13521352
return ['misc'];
13531353
});
1354+
vi.mocked(mockConnector.getViews).mockImplementation(async (schema?: string) => {
1355+
if (schema === 'configured_db') return ['order_summary'];
1356+
if (schema === 'other_db') return ['secret_view'];
1357+
return ['misc_view'];
1358+
});
13541359
});
13551360

13561361
it('scopes table search to the default schema and never fans out to other databases', async () => {
@@ -1370,6 +1375,40 @@ describe('search_database_objects tool', () => {
13701375
expect(mockConnector.getTables).toHaveBeenCalledWith('configured_db');
13711376
});
13721377

1378+
it('scopes view search to the default schema and never fans out to other databases', async () => {
1379+
vi.mocked((mockConnector as any).getDefaultSchema).mockResolvedValue('configured_db');
1380+
1381+
const handler = createSearchDatabaseObjectsToolHandler();
1382+
const result = await handler(
1383+
{ object_type: 'view', pattern: '%', detail_level: 'names' },
1384+
null
1385+
);
1386+
1387+
const parsed = parseToolResponse(result);
1388+
expect(parsed.data.results.map((r: any) => r.schema)).toEqual(['configured_db']);
1389+
expect(parsed.data.results.map((r: any) => r.name)).toEqual(['order_summary']);
1390+
// Only the configured database should have been inspected.
1391+
expect(mockConnector.getViews).toHaveBeenCalledTimes(1);
1392+
expect(mockConnector.getViews).toHaveBeenCalledWith('configured_db');
1393+
});
1394+
1395+
it('falls back to the full schema list for views when no default is configured (null)', async () => {
1396+
vi.mocked((mockConnector as any).getDefaultSchema).mockResolvedValue(null);
1397+
1398+
const handler = createSearchDatabaseObjectsToolHandler();
1399+
const result = await handler(
1400+
{ object_type: 'view', pattern: '%', detail_level: 'names' },
1401+
null
1402+
);
1403+
1404+
const parsed = parseToolResponse(result);
1405+
expect(parsed.data.results.map((r: any) => r.schema)).toEqual([
1406+
'configured_db',
1407+
'other_db',
1408+
'third_db',
1409+
]);
1410+
});
1411+
13731412
it('scopes schema listing to the default schema', async () => {
13741413
vi.mocked((mockConnector as any).getDefaultSchema).mockResolvedValue('configured_db');
13751414

src/tools/search-objects.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ async function searchViews(
296296
if (schemaFilter) {
297297
schemasToSearch = [schemaFilter];
298298
} else {
299-
schemasToSearch = await connector.getSchemas();
299+
schemasToSearch = await resolveDefaultSchemas(connector);
300300
}
301301

302302
// Search views in each schema

0 commit comments

Comments
 (0)