Skip to content

Add dynamic MCP tools: list_indexes, index_repo, delete_index#15

Draft
richhankins wants to merge 24 commits intomainfrom
dynamic-mcp-tools
Draft

Add dynamic MCP tools: list_indexes, index_repo, delete_index#15
richhankins wants to merge 24 commits intomainfrom
dynamic-mcp-tools

Conversation

@richhankins
Copy link
Contributor

@richhankins richhankins commented Feb 2, 2026

Overview

This PR adds dynamic index discovery to the context-connectors MCP server, enabling agents to see new indexes as they're added by users without restarting the MCP server.

Two Modes

Mode CLI Description
Fixed ctxc mcp stdio -i pytorch -i react Static index list from CLI args
Discovery ctxc mcp stdio --discovery Dynamic index list from filesystem

Tool Availability

Tool Fixed Mode Discovery Mode
search ✅ with enum ✅ no enum
list_files ✅ with enum ✅ no enum
read_file ✅ with enum ✅ no enum
list_indexes ✅ (returns static list) ✅ (required for discovery)
index_repo
delete_index

Note: In Discovery mode, users manage indexes via CLI (ctxc index ... commands), not via MCP tools.

Key Changes

1. Dynamic MultiIndexRunner

  • Removed readonly from indexNames and indexes
  • Added refreshIndexList() method to reload from store
  • Added invalidateClient(name) to clear cached SearchClient
  • Allowed empty index list on startup

2. list_indexes Tool

  • New MCP tool to list available indexes with metadata
  • Available in both Fixed and Discovery modes
  • Returns index name, source, and last sync time

3. Mode-Based Tool Descriptions

  • Fixed mode: Includes enum in tool schemas for zero-tool-call discovery
  • Discovery mode: No enum, agent must call list_indexes
  • Prevents stale enum values in discovery mode

4. CLI Flag

  • Added --discovery flag for explicit mode selection
  • Default (no flags) uses FilesystemStore in discovery mode
  • -i flags use CompositeStoreReader in fixed mode

Benefits

  1. User controls lifecycle - No accidental index creation/deletion by agents
  2. No concurrency issues - Only user writes to the store via CLI
  3. Simpler implementation - No write operations in MCP server
  4. Incremental improvement - Minimal changes to existing MCP configs
  5. Clear separation - Agent discovers, user manages

Example Workflow

Agent: "I need React Query docs to answer this question"
Agent: → list_indexes() → Not found
Agent: "I don't have React Query indexed. Would you like me to help you index it?"

User: "Yes"
User: $ ctxc index github TanStack/query

Agent: → list_indexes() → Now sees "tanstack-query"
Agent: → search("tanstack-query", "...")

Testing

  • ✅ All 163 tests pass
  • ✅ TypeScript compiles cleanly
  • ✅ Manual testing via CLI and MCP tools
  • ✅ Verified both Fixed and Discovery modes

Future Work

Discovery mode is an incremental step. A future PR can add full agent-managed mode with:

  • index_repo and delete_index tools
  • Writable FilesystemStore
  • Optional LayeredStore for remote + local indexes
  • Concurrency handling (lock files)

- Remove readonly from indexNames and indexes fields
- Remove empty-index checks at initialization (allow zero indexes)
- Add refreshIndexList() method to reload indexes from store
- Add invalidateClient() method to clear cached SearchClient
- Update MCPServerConfig.store to accept both IndexStoreReader and IndexStore

Agent-Id: agent-49436113-35bf-4532-983a-fb01da818555
- Add list_indexes tool to ListToolsRequestSchema handler
- Add handler in CallToolRequestSchema that refreshes index list and
  returns formatted metadata (name, type, identifier, syncedAt)
- Returns helpful message when no indexes exist
- Add withListIndexesReference() helper in tool-descriptions.ts
- Update MCP server to use withListIndexesReference instead of
  embedding static index list in tool descriptions
- Keep withIndexList() for CLI agent backward compatibility

Agent-Id: agent-b187383f-2e44-4592-9ec9-6f0f4e0eafb6
- Add delete_index tool definition (only when store supports delete)
- Add handler to delete index from store, refresh runner state, and invalidate cached client
- Validates index exists before attempting delete
- Returns appropriate error messages for missing index or unsupported store

Agent-Id: agent-8cf675a5-fcdf-402a-a3a1-f3fa2a8c3f1b
Agent-Id: agent-8cf675a5-fcdf-402a-a3a1-f3fa2a8c3f1b
Linked-Note-Id: 18ee4796-4931-48c2-80e4-e85d51fe2ccd
This tool allows creating/updating indexes dynamically from:
- GitHub repositories (requires owner and repo)
- GitLab repositories (requires project_id)
- BitBucket repositories (requires workspace and repo)
- Websites (requires url)

Features:
- Validates required parameters for each source type
- Checks if store supports write operations
- Refreshes runner state after indexing
- Invalidates cached client for updated indexes
- Returns success message with stats (type, filesIndexed, duration)

Agent-Id: agent-4dd9b0b4-3ecf-4a2a-850a-49fd55aba0b5
Remove the empty-index check from the CLI so the MCP server can start
even when no indexes exist. This enables the dynamic indexing workflow
where users can call list_indexes (empty) then index_repo to create
indexes on-demand.
Agent-Id: agent-9c6981c4-45dd-4c45-bec4-386d78c4bd30
Agent-Id: agent-49ab3e98-e947-4ed1-8796-9454c317bd15
Agent-Id: agent-e9ca9165-d91d-4f53-a17c-da25a2a71927
Linked-Note-Id: 8e3113fe-0ce3-4374-81ed-e0536c4bbe35
Agent-Id: agent-bb8e0202-acb6-4479-940b-8cb958aa88f1
Linked-Note-Id: 5039c3be-a4e3-4fed-a279-827b12453884
The agentManaged flag was correctly calculated at the top of both stdio and http handlers, but then recalculated incorrectly when passing to the MCP server config. This caused the --agent-managed flag to be ignored when combined with -i specs.

Changes:
- Line 56 (stdio): Use calculated agentManaged variable instead of recalculating
- Line 130 (http): Use calculated agentManaged variable instead of recalculating

This ensures --agent-managed -i s3://... correctly sets agentManaged: true in the server config.

Agent-Id: agent-1fdc5885-eb2c-4a33-9451-323cf14bb29a
- Replace --agent-managed flag with --discovery in cmd-mcp.ts
- Remove index_repo and delete_index tool handlers from mcp-server.ts
- Delete layered-store.ts and layered-store.test.ts files
- Keep list_indexes in both modes
- Use withListIndexesReference() in discovery mode, withIndexList() in fixed mode
- Update MCPServerConfig to use discovery flag instead of agentManaged
- All tests pass, build succeeds

Agent-Id: agent-84f44748-891f-477f-b3f3-c078cb5bd2c5
Agent-Id: agent-84f44748-891f-477f-b3f3-c078cb5bd2c5
Linked-Note-Id: 75b599ae-9ec2-4535-b5d2-f83cc77a9d6b
- Create ReadOnlyLayeredStore to merge local (FilesystemStore) and remote (CompositeStoreReader) indexes
- Update cmd-mcp.ts to use ReadOnlyLayeredStore when both --discovery and -i flags are present
- Add comprehensive tests for ReadOnlyLayeredStore (12 tests)
- Export ReadOnlyLayeredStore from stores/index.ts

Behavior:
- ctxc mcp stdio --discovery -i s3://bucket/shared-index: Merges local + remote indexes
- ctxc mcp stdio -i pytorch -i react: Fixed mode (no discovery)
- ctxc mcp stdio --discovery: Discovery mode with local indexes only

All tests pass (175 passed, 24 skipped)

Agent-Id: agent-5834efcf-335b-415a-b8a9-19ed489133ed
Merge commits:
- ff69f62: Fix search/list/read operations to use indexed commit ref (#13)
- ef6a46a: feat: add consistent User-Agent string (#14)

Resolved conflicts:
- src/clients/mcp-server.ts: Kept Discovery mode logic while integrating User-Agent changes
- src/clients/multi-index-runner.ts: Added clientUserAgent parameter and updateClientUserAgent method

Fixed type errors from main branch:
- Removed invalid clientUserAgent parameter from DirectContext.create/import calls
- Removed invalid clientUserAgent parameter from AugmentLanguageModel config

All tests pass and build succeeds.
The main branch code attempted to pass clientUserAgent to DirectContext.create/import
and AugmentLanguageModel, but these APIs don't accept that parameter.

Changes:
- Removed clientUserAgent from DirectContext.create() call in indexer.ts
- Removed clientUserAgent from DirectContext.import() calls in indexer.ts and search-client.ts
- Removed clientUserAgent from AugmentLanguageModel config in cli-agent.ts

The clientUserAgent is still stored in SearchClient and Indexer for future use,
but is not passed to APIs that don't support it.
@richhankins
Copy link
Contributor Author

augment review

@augment-app-staging
Copy link

augment-app-staging bot commented Feb 6, 2026

🤖 Augment PR Summary

Summary: This PR adds a read-only “discovery mode” to the MCP server so agents can dynamically discover available indexes at runtime via a new list_indexes tool.

Changes:

  • Adds a --discovery flag to ctxc mcp stdio and ctxc mcp http, and wires a discovery boolean through to the MCP server config.
  • Introduces list_indexes MCP tool and adjusts tool descriptions/schemas: fixed mode includes an enum of index names, discovery mode omits it and points clients to list_indexes.
  • Extends MultiIndexRunner with refreshIndexList() (dynamic list in discovery mode) and cache pruning, plus invalidateClient().
  • Adds ReadOnlyLayeredStore to merge local filesystem indexes with remote composite indexes for discovery workflows.
  • Adds unit tests covering list_indexes presence and fixed-vs-discovery schema behavior, plus runner refresh behavior.

Technical Notes: Discovery mode is intentionally read-only; index creation/deletion remains a CLI responsibility (ctxc index ...) while the MCP server focuses on search and file read tooling.

🤖 Was this summary useful? React with 👍 or 👎

Copy link

@augment-app-staging augment-app-staging bot left a comment

Choose a reason for hiding this comment

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

Review completed. 4 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

} else {
// No --index: use default store, list all indexes
// Discovery mode only: use FilesystemStore
store = new FilesystemStore();

Choose a reason for hiding this comment

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

This branch sets up discovery mode without requiring any indexes, but MultiIndexRunner.create() still throws when the store has zero valid indexes, so ctxc mcp ... can still fail to start on a fresh install. If discovery mode is intended to allow “start empty and index later”, consider aligning runner/server startup behavior with that expectation.

Severity: high

Other Locations
  • src/bin/cmd-mcp.ts:106

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.


// Handle list_indexes separately (no index_name required)
if (name === "list_indexes") {
await runner.refreshIndexList();

Choose a reason for hiding this comment

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

list_indexes is handled before the surrounding try/catch, so failures in refreshIndexList() (e.g., store I/O errors) may escape and fail the MCP request differently than other tools. Consider applying the same error handling behavior as the search/list_files/read_file branches.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

const { indexes } = runner;
if (indexes.length === 0) {
return {
content: [{ type: "text", text: "No indexes available. Use index_repo to create one." }],

Choose a reason for hiding this comment

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

This response says “Use index_repo to create one”, but this server doesn’t expose index_repo (and the PR description says index management is via CLI in discovery mode). Consider pointing users to the appropriate ctxc index ... CLI workflow instead to avoid confusion.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

version?: string;
/**
* Discovery mode flag.
* When true: use withListIndexesReference (no enum in schemas), no write tools

Choose a reason for hiding this comment

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

The discovery docstring says “write tools available” when false, but this server doesn’t currently expose any write tools (e.g., index_repo/delete_index). Consider updating the comment to match actual tool availability to prevent misleading integrators.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

…docs

Agent-Id: agent-8f86dfa1-8342-4d24-9127-87412acf9740
@richhankins
Copy link
Contributor Author

augment review

All 4 review comments from the previous review have been addressed:

  1. ✅ Empty indexes now allowed in discovery mode
  2. list_indexes handler wrapped in try/catch
  3. ✅ Message now says "Use ctxc index CLI" instead of index_repo
  4. ✅ Docstring updated to reflect actual behavior (no write tools)

Copy link

@augment-app-staging augment-app-staging bot left a comment

Choose a reason for hiding this comment

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

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

}
}

this.indexNames = newIndexNames;

Choose a reason for hiding this comment

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

refreshIndexList() overwrites this.indexNames from store.list(), which can override an initially configured indexNames allowlist (fixed mode) and unintentionally widen what getClient() will accept after a refresh. If fixed mode is meant to stay static, consider keeping the originally requested list separate from the refreshed/discovered list.

Severity: medium

Other Locations
  • src/clients/mcp-server.ts:300

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools: Tool[] = [
{
name: "list_indexes",

Choose a reason for hiding this comment

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

The new list_indexes tool + the discovery-vs-fixed behavior (no enum vs enum for index_name) doesn’t appear to be covered by existing MCP server tests; a focused unit test here could help prevent regressions.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

In fixed mode (when indexNames are specified via CLI), calling list_indexes
would trigger refreshIndexList() which overwrote this.indexNames with ALL
indexes from the store, bypassing the original allowlist.

Changes:
- Add readonly originalIndexNames field to MultiIndexRunner to store the
  original allowlist in fixed mode
- In constructor, save the original indexNames if provided
- In refreshIndexList(), filter results to only include indexes in the
  original allowlist when in fixed mode

This ensures that in fixed mode (-i pytorch -i react), list_indexes only
returns the originally specified indexes, even if other indexes exist in
the store. In discovery mode (--discovery), all indexes are returned as
before.

Tests added to verify:
- Discovery mode includes all indexes from store
- Fixed mode respects the original allowlist
- Fixed mode handles missing indexes gracefully

Agent-Id: agent-cf0760a8-47e4-4742-a697-7de4bf2367e6
In fixed mode, refreshIndexList() is now a no-op. The index list is
completely static and never changes, even if indexes are deleted from
the store.

This fixes the issue where:
1. Fixed mode starts with [-i pytorch -i react]
2. User deletes pytorch from the store
3. Agent calls list_indexes → triggers refreshIndexList()
4. Previously: indexNames would shrink to ["react"]
5. Now: indexNames remains ["pytorch", "react"]

In discovery mode, refreshIndexList() still refreshes from the store
to pick up new/deleted indexes.

Changes:
- src/clients/multi-index-runner.ts: Make refreshIndexList() a no-op in fixed mode
- src/clients/multi-index-runner.test.ts: Update test to expect static list in fixed mode

All tests pass, build succeeds.

Agent-Id: agent-0ac871dd-5181-4ed9-84d1-446aa4b19c81
@richhankins
Copy link
Contributor Author

augment review

Changes since last review:

  1. Fixed refreshIndexList() allowlist bypass (medium severity) - In fixed mode, refreshIndexList() is now a complete no-op. The index list is set at startup and never changes, even if indexes are added or deleted from the store.

  2. Fixed mode is completely static - Calling list_indexes in fixed mode always returns exactly the indexes specified via -i flags, regardless of store state.

Copy link

@augment-app-staging augment-app-staging bot left a comment

Choose a reason for hiding this comment

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

Review completed. 3 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

}

return new MultiIndexRunner(store, validIndexNames, indexes, searchOnly, config.clientUserAgent);
// Allow empty - server can start with no indexes and user can add via CLI

Choose a reason for hiding this comment

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

MultiIndexRunner.create() now allows returning an empty validIndexNames even when config.indexNames was explicitly provided (fixed mode), which can make the server start “successfully” but reject all tool calls. Consider whether fixed mode should still fail fast when none of the requested indexes can be loaded (e.g., due to corruption/partial state).

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

}
}

this.indexNames = newIndexNames;

Choose a reason for hiding this comment

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

In discovery mode, refreshIndexList() updates indexNames/indexes but doesn’t prune or invalidate entries in clientCache, so a re-indexed name could keep serving stale data (and removed indexes can remain resident in memory). Consider whether refresh should reconcile the cache with the refreshed list/metadata.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

}

// Import DirectContext from saved state
this.context = await DirectContext.import(this.state.contextState, {

Choose a reason for hiding this comment

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

SearchClientConfig.clientUserAgent is still accepted/stored, but it’s no longer passed into DirectContext.import(), so the option appears to be a no-op while docs say it’s “sent to the Augment API”. Consider either wiring it through again (if supported) or removing/updating the option/docs to avoid misleading callers.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Agent-Id: agent-42c9e35e-86c9-4eea-abf2-253ad24da923
Agent-Id: agent-81feb144-5dc6-4a1f-9cd1-7e74cd26e3d1
Agent-Id: agent-02660ea0-b190-4940-802f-62ed81e5543d
Agent-Id: agent-5dbe4a24-0e78-4075-b85c-d6ff632c88c7
Agent-Id: agent-9eaf775a-04cb-45c8-bc31-4705c63e8b54
@richhankins
Copy link
Contributor Author

augment review

Copy link

@augment-app-staging augment-app-staging bot left a comment

Choose a reason for hiding this comment

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

Review completed. 3 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.


if (indexSpecs && indexSpecs.length > 0) {
// Parse index specs and create composite store
if (discoveryFlag && indexSpecs && indexSpecs.length > 0) {

Choose a reason for hiding this comment

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

--discovery is currently only honored when --index is also provided; ctxc mcp stdio --discovery (without -i) falls through to the “No flags” branch and sets discovery = false, so discovery mode never activates.

Severity: high

Other Locations
  • src/bin/cmd-mcp.ts:98

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

it("handles errors gracefully and returns isError: true", async () => {
const store = createMockStore(createMockState());
// Mock store.list() to throw an error
store.list = vi.fn().mockRejectedValue(new Error("Store error"));

Choose a reason for hiding this comment

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

This test mocks store.list() to reject before calling createMCPServer(), but createMCPServer()/MultiIndexRunner.create() calls store.list() during startup, so the server will likely fail to construct and the test won’t actually exercise the list_indexes error handling path.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools: Tool[] = [
{
name: "list_indexes",

Choose a reason for hiding this comment

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

PR title mentions index_repo/delete_index, but this server only exposes list_indexes (and the PR description says write tools are unavailable). Consider aligning the PR title (and any related docs) with the actual tool set to avoid confusion for integrators.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

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.

1 participant