Skip to content

feat: reduce context complexity multi db#277

Open
MarlzRana wants to merge 9 commits intobytebase:mainfrom
MarlzRana:marlzrana/reduce-context-complexity-multi-db
Open

feat: reduce context complexity multi db#277
MarlzRana wants to merge 9 commits intobytebase:mainfrom
MarlzRana:marlzrana/reduce-context-complexity-multi-db

Conversation

@MarlzRana
Copy link
Copy Markdown

@MarlzRana MarlzRana commented Mar 16, 2026

At moment, the context complexity of this MCP server is O(n), where n is the number of sources. As you can imagine in an organization with multiple databases, this causes context bloat.

Let's bring down the context complexity of the MCP server, down to O(1), in multi-source mode, by introducing a list_sources meta tool, that is used to get a source_id per source, that agents pass as an argument to the other tools (i.e. execute_sql/ describe_objects), to execute a particular operation against a particular source.

Really awesome MCP server btw! 😄

import LockIcon from '../icons/LockIcon';
import CopyIcon from '../icons/CopyIcon';
import CheckIcon from '../icons/CheckIcon';
import { useEffect, useState, useCallback, useRef } from "react";
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linter ran on this file when I edited via VSCode - perhaps it is worth adding a Claude Code hook, to also do this after an edit.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the file change is the linter, but there are also functional changes.

</Link>
<div className="flex-1 overflow-auto">
{currentSource.tools
.filter((tool) => tool.name !== 'search_objects')
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already a WIP notice in the tool detail - let's still present it in the sidebar.

@MarlzRana
Copy link
Copy Markdown
Author

MarlzRana commented Mar 29, 2026

Ready when you are @tianzhou! 😄

Let's first review the functional and test changes, then I'll get Claude to update the docs as well.

@MarlzRana MarlzRana changed the title wip: reduce context complexity multi db feat: reduce context complexity multi db Mar 29, 2026
@tianzhou tianzhou requested a review from Copilot March 30, 2026 03:27
export function getExecuteSqlMetadata(sourceId: string): ToolMetadata {
const sourceIds = ConnectorManager.getAvailableSourceIds();
const sourceConfig = ConnectorManager.getSourceConfig(sourceId)!;
export function getExecuteSqlMetadata(boundSourceId?: string): ToolMetadata {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's strange that boundSourceId can be nil. Introducing list_source shouldn't change this behavior.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR reduces MCP tool “context bloat” in multi-source configurations by switching from per-source tool instances to a small set of generic tools that accept a source_id, plus a new list_sources meta-tool for discovery.

Changes:

  • Add list_sources meta-tool and register only generic execute_sql / search_objects in multi-source mode.
  • Introduce multi-source Zod schemas that require source_id, and adjust tool metadata generation accordingly.
  • Update backend/frontend tests and UI to handle generic tool names and source_id binding.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/utils/tool-metadata.ts Generates generic tool metadata + schemas for multi-source mode; improves Zod-to-parameter mapping.
src/utils/tests/tool-metadata.test.ts Adds unit tests for new metadata + zodToParameters behavior.
src/tools/execute-sql.ts Adds multi-source schema + source_id argument support in handler.
src/tools/search-objects.ts Adds multi-source schema + source_id argument support in handler.
src/tools/list-sources.ts New list_sources meta-tool implementation.
src/tools/index.ts Registers generic tools + list_sources in multi-source mode; custom tools remain per-source.
src/tools/builtin-tools.ts Introduces meta-tool constant and combined builtin/meta list.
src/tools/registry.ts Updates builtin name handling and adds getCustomToolsForSource.
src/tools/tests/execute-sql.test.ts Adds tests for source_id resolution precedence behavior.
src/tools/tests/registry.test.ts Tests meta-tool naming conflict protection + custom tool filtering.
src/tools/tests/list-sources.test.ts Tests list_sources registration and response shape.
src/config/toml-loader.ts Updates builtin detection to include meta-tools.
src/api/tests/sources.integration.test.ts Updates expectations for generic tool names + source_id parameter exposure.
frontend/src/components/views/ToolDetailView.tsx Binds source_id automatically for generic tools; hides source_id from editable params.
frontend/src/components/views/SourceDetailView.tsx Displays bound source_id parameter distinctly in tool parameter lists.
frontend/src/components/Sidebar/Sidebar.tsx Shows search_objects in the sidebar tool list (no longer filtered out).
Comments suppressed due to low confidence (1)

src/tools/execute-sql.ts:58

  • In multi-source mode this handler can execute against a source where execute_sql is disabled in the ToolRegistry: getBuiltinToolConfig(...) will return undefined, but the code still executes with no readonly/max_rows constraints. Please explicitly reject execution when toolConfig is missing for actualSourceId (i.e., tool not enabled for that source).
      // Get tool-specific configuration (tool is already registered, so it's enabled)
      const registry = getToolRegistry();
      const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, actualSourceId);

Comment on lines 503 to +533
@@ -499,25 +509,28 @@ export function createSearchDatabaseObjectsToolHandler(sourceId?: string) {
table,
detail_level = "names",
limit = 100,
source_id: argSourceId,
} = args as {
object_type: DatabaseObjectType;
pattern?: string;
schema?: string;
table?: string;
detail_level: DetailLevel;
limit: number;
source_id?: string;
};

const resolvedSourceId = boundSourceId ?? argSourceId;
const startTime = Date.now();
const effectiveSourceId = getEffectiveSourceId(sourceId);
const effectiveSourceId = getEffectiveSourceId(resolvedSourceId);
let success = true;
let errorMessage: string | undefined;

try {
// Ensure source is connected (handles lazy connections)
await ConnectorManager.ensureConnected(sourceId);
await ConnectorManager.ensureConnected(resolvedSourceId);

const connector = ConnectorManager.getCurrentConnector(sourceId);
const connector = ConnectorManager.getCurrentConnector(resolvedSourceId);
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolvedSourceId may be undefined if the caller omits source_id, which will default to the first configured source via ensureConnected(undefined)/getCurrentConnector(undefined). In multi-source mode this is ambiguous and can lead to querying the wrong DB. Consider returning a validation error when multiple sources are configured and source_id is missing.

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +69
// Unwrap ZodDefault/ZodOptional
if (innerType instanceof z.ZodDefault) {
required = false;
innerType = (innerType as z.ZodDefault<any>)._def.innerType;
}

if (innerType instanceof z.ZodOptional) {
required = false;
innerType = (innerType as z.ZodOptional<any>).unwrap();
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zodToParameters unwraps ZodDefault via the private ._def.innerType, which is not part of Zod’s public API and can break on upgrades. Prefer the public removeDefault() method (and then continue unwrapping optionals) to avoid relying on internals.

Copilot uses AI. Check for mistakes.
Comment on lines +212 to +213
// Parameters the user can actually fill in (excludes bound source_id)
const editableParameters = tool?.parameters.filter((p) => p.name !== "source_id") ?? [];
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UI treats any parameter named source_id as a bound, non-editable field by filtering it out of editableParameters. This will break any custom tool that legitimately defines a source_id parameter (it becomes impossible to set and will never be sent). Consider scoping this behavior to the built-ins (execute_sql/search_objects) in multi-source mode, or reserving/disallowing source_id as a custom-tool parameter name at config validation time.

Suggested change
// Parameters the user can actually fill in (excludes bound source_id)
const editableParameters = tool?.parameters.filter((p) => p.name !== "source_id") ?? [];
// Parameters the user can actually fill in.
// For non-custom tools in multi-source mode, source_id is treated as a bound param
// and therefore excluded from the editable set. Custom tools may freely use source_id.
const editableParameters =
tool?.parameters.filter((p) => !(toolType !== "custom" && p.name === "source_id")) ?? [];

Copilot uses AI. Check for mistakes.
Comment on lines 33 to 36
* Create an execute_sql tool handler for a specific source
* @param sourceId - The source ID this handler is bound to (undefined for single-source mode)
* @param boundSourceId - The source ID this handler is bound to (undefined for single-source mode)
* @returns A handler function bound to the specified source
*/
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSDoc says boundSourceId is undefined for single-source mode, but registerTools() passes a concrete source ID in single-source mode and undefined in multi-source mode (generic tool). Please update the comment to match actual usage to avoid confusion.

Copilot uses AI. Check for mistakes.
Comment on lines +500 to +501
* @param boundSourceId - The source ID this handler is bound to (undefined for single-source mode)
* @returns A handler function bound to the specified source
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSDoc says boundSourceId is undefined for single-source mode, but registerTools() passes a concrete source ID in single-source mode and undefined in multi-source mode (generic tool). Please update the comment to reflect the actual binding behavior.

Suggested change
* @param boundSourceId - The source ID this handler is bound to (undefined for single-source mode)
* @returns A handler function bound to the specified source
* @param boundSourceId - The source ID this handler is bound to in single-source mode. If undefined, the handler
* is generic and will use the source_id argument provided at call time (multi-source mode).
* @returns A handler function bound to the specified source (or generic if no source is bound)

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +52
const { sql, source_id: argSourceId } = args as { sql: string; source_id?: string };
const resolvedSourceId = boundSourceId ?? argSourceId;
const startTime = Date.now();
const effectiveSourceId = getEffectiveSourceId(sourceId);
const effectiveSourceId = getEffectiveSourceId(resolvedSourceId);
let success = true;
let errorMessage: string | undefined;
let result: any;

try {
// Ensure source is connected (handles lazy connections)
await ConnectorManager.ensureConnected(sourceId);
await ConnectorManager.ensureConnected(resolvedSourceId);

// Get connector for the specified source (or default)
const connector = ConnectorManager.getCurrentConnector(sourceId);
const connector = ConnectorManager.getCurrentConnector(resolvedSourceId);
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolvedSourceId can be undefined (e.g., if the caller omits source_id), which will silently fall back to the default source via ensureConnected(undefined)/getCurrentConnector(undefined). In multi-source mode this is risky and contradicts the schema comment that source_id is required. Consider failing fast when multiple sources are configured and no source_id is provided (return a clear tool error).

Copilot uses AI. Check for mistakes.
Comment on lines +523 to 534
const resolvedSourceId = boundSourceId ?? argSourceId;
const startTime = Date.now();
const effectiveSourceId = getEffectiveSourceId(sourceId);
const effectiveSourceId = getEffectiveSourceId(resolvedSourceId);
let success = true;
let errorMessage: string | undefined;

try {
// Ensure source is connected (handles lazy connections)
await ConnectorManager.ensureConnected(sourceId);
await ConnectorManager.ensureConnected(resolvedSourceId);

const connector = ConnectorManager.getCurrentConnector(sourceId);
const connector = ConnectorManager.getCurrentConnector(resolvedSourceId);

Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In multi-source mode this handler can run against a source where search_objects is disabled because it never checks tool enablement per resolvedSourceId/connector ID, and registerTools() registers the generic search_objects unconditionally. Please consult the ToolRegistry for the selected source and return an error when search_objects is not enabled for that source.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants