Skip to content
Draft
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4981a79
Support dynamic index list in MultiIndexRunner
richhankins Feb 2, 2026
427a1bc
Add list_indexes tool to MCP server
richhankins Feb 2, 2026
6447099
Add delete_index tool to MCP server
richhankins Feb 2, 2026
0592040
Add delete_index Tool
richhankins Feb 2, 2026
2ea4cdf
Add index_repo tool to MCP server
richhankins Feb 2, 2026
35f06b1
Allow MCP server to start with zero indexes
richhankins Feb 2, 2026
94d91f1
Fix index_repo conditional and remove stale enum from MCP tools
richhankins Feb 6, 2026
4316cd4
Implement LayeredStore class combining local and remote indexes
richhankins Feb 6, 2026
e0fc0f7
Restore enum in Fixed Mode
richhankins Feb 6, 2026
975991f
Add --agent-managed CLI Flag
richhankins Feb 6, 2026
bbed4fc
Fix agentManaged config calculation in cmd-mcp.ts
richhankins Feb 6, 2026
b781515
Replace agent-managed with discovery mode
richhankins Feb 6, 2026
44d7569
Replace Agent-Managed with Discovery Mode
richhankins Feb 6, 2026
dcf2386
Add support for -i flags in Discovery mode with ReadOnlyLayeredStore
richhankins Feb 6, 2026
81b4dbf
Merge origin/main into dynamic-mcp-tools
richhankins Feb 6, 2026
c932344
Fix type errors from main branch merge
richhankins Feb 6, 2026
ba57527
Fix PR review comments: allow empty indexes, add error handling, fix …
richhankins Feb 6, 2026
2c37861
Fix refreshIndexList() to respect fixed mode allowlist
richhankins Feb 6, 2026
c9eed14
Make fixed mode's index list completely static
richhankins Feb 6, 2026
c9c4ea1
fix: restore clientUserAgent in DirectContext.import()
richhankins Feb 7, 2026
5c35f78
fix: prune stale clientCache entries in refreshIndexList()
richhankins Feb 7, 2026
3220fcf
fix: restore fail-fast behavior for non-discovery mode (issues #1, #7)
richhankins Feb 7, 2026
733ad36
test: add unit tests for list_indexes and discovery vs fixed mode
richhankins Feb 7, 2026
9c79c29
fix: restore clientUserAgent in AugmentLanguageModel constructor
richhankins Feb 7, 2026
73096b1
fix: honor --discovery flag without --index
richhankins Feb 9, 2026
1dad310
refactor: simplify store type to IndexStoreReader (MCP only reads)
richhankins Feb 9, 2026
6070a77
fix: restore clientUserAgent in Indexer DirectContext calls
richhankins Feb 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 39 additions & 23 deletions src/bin/cmd-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { FilesystemStore } from "../stores/filesystem.js";
import { runMCPServer } from "../clients/mcp-server.js";
import { parseIndexSpecs } from "../stores/index-spec.js";
import { CompositeStoreReader } from "../stores/composite.js";
import { ReadOnlyLayeredStore } from "../stores/read-only-layered-store.js";

// stdio subcommand (stdio-based MCP server for local clients like Claude Desktop)
const stdioCommand = new Command("stdio")
Expand All @@ -15,36 +16,44 @@ const stdioCommand = new Command("stdio")
"-i, --index <specs...>",
"Index spec(s): name, path:/path, or s3://bucket/key"
)
.option("--discovery", "Enable discovery mode (read-only, manage indexes via CLI)")
.option("--search-only", "Disable list_files/read_file tools (search only)")
.action(async (options) => {
try {
const indexSpecs: string[] | undefined = options.index;
const discoveryFlag = options.discovery;

let store;
let indexNames: string[];
let indexNames: string[] | undefined;
let discovery: boolean;

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

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.

// Discovery mode WITH remote indexes: merge local + remote
const specs = parseIndexSpecs(indexSpecs);
const remoteStore = await CompositeStoreReader.fromSpecs(specs);
const localStore = new FilesystemStore();
store = new ReadOnlyLayeredStore(localStore, remoteStore);
indexNames = undefined; // Discovery mode: no fixed list
discovery = true;
} else if (indexSpecs && indexSpecs.length > 0) {
// Fixed mode: use read-only CompositeStoreReader
const specs = parseIndexSpecs(indexSpecs);
store = await CompositeStoreReader.fromSpecs(specs);
indexNames = specs.map((s) => s.displayName);
store = await CompositeStoreReader.fromSpecs(specs);
discovery = false;
} else {
// No --index: use default store, list all indexes
// Discovery mode only: use FilesystemStore
store = new FilesystemStore();
Copy link
Copy Markdown

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.

indexNames = await store.list();
if (indexNames.length === 0) {
console.error("Error: No indexes found.");
console.error("The MCP server requires at least one index to operate.");
console.error("Run 'ctxc index --help' to see how to create an index.");
process.exit(1);
}
indexNames = undefined;
discovery = true;
}

// Start MCP server (writes to stdout, reads from stdin)
await runMCPServer({
store,
indexNames,
searchOnly: options.searchOnly,
discovery,
});
} catch (error) {
// Write errors to stderr (stdout is for MCP protocol)
Expand All @@ -60,6 +69,7 @@ const httpCommand = new Command("http")
"-i, --index <specs...>",
"Index spec(s): name, path:/path, or s3://bucket/key"
)
.option("--discovery", "Enable discovery mode (read-only, manage indexes via CLI)")
.option("--port <number>", "Port to listen on", "3000")
.option("--host <host>", "Host to bind to", "localhost")
.option("--cors <origins>", "CORS origins (comma-separated, or '*' for any)")
Expand All @@ -72,26 +82,31 @@ const httpCommand = new Command("http")
.action(async (options) => {
try {
const indexSpecs: string[] | undefined = options.index;
const discoveryFlag = options.discovery;

let store;
let indexNames: string[] | undefined;
let discovery: boolean;

if (indexSpecs && indexSpecs.length > 0) {
// Parse index specs and create composite store
if (discoveryFlag && indexSpecs && indexSpecs.length > 0) {
// Discovery mode WITH remote indexes: merge local + remote
const specs = parseIndexSpecs(indexSpecs);
const remoteStore = await CompositeStoreReader.fromSpecs(specs);
const localStore = new FilesystemStore();
store = new ReadOnlyLayeredStore(localStore, remoteStore);
indexNames = undefined; // Discovery mode: no fixed list
discovery = true;
} else if (indexSpecs && indexSpecs.length > 0) {
// Fixed mode: use read-only CompositeStoreReader
const specs = parseIndexSpecs(indexSpecs);
store = await CompositeStoreReader.fromSpecs(specs);
indexNames = specs.map((s) => s.displayName);
store = await CompositeStoreReader.fromSpecs(specs);
discovery = false;
} else {
// No --index: use default store, serve all
// Discovery mode only: use FilesystemStore
store = new FilesystemStore();
const availableIndexes = await store.list();
if (availableIndexes.length === 0) {
console.error("Error: No indexes found.");
console.error("The MCP server requires at least one index to operate.");
console.error("Run 'ctxc index --help' to see how to create an index.");
process.exit(1);
}
indexNames = undefined;
discovery = true;
}

// Parse CORS option
Expand All @@ -112,6 +127,7 @@ const httpCommand = new Command("http")
store,
indexNames,
searchOnly: options.searchOnly,
discovery,
port: parseInt(options.port, 10),
host: options.host,
cors,
Expand Down
1 change: 0 additions & 1 deletion src/clients/cli-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,6 @@ async function loadModel(
return new AugmentLanguageModel(modelName, {
apiKey: credentials.apiKey,
apiUrl: credentials.apiUrl,
clientUserAgent,
}) as unknown as LanguageModel;
}
default:
Expand Down
70 changes: 60 additions & 10 deletions src/clients/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,23 @@ import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import type { IndexStoreReader } from "../stores/types.js";
import type { IndexStoreReader, IndexStore } from "../stores/types.js";
import type { Source } from "../sources/types.js";
import { MultiIndexRunner } from "./multi-index-runner.js";
import { buildClientUserAgent, type MCPClientInfo } from "../core/utils.js";
import {
SEARCH_DESCRIPTION,
LIST_FILES_DESCRIPTION,
READ_FILE_DESCRIPTION,
withListIndexesReference,
withIndexList,
} from "./tool-descriptions.js";
/**
* Configuration for the MCP server.
*/
export interface MCPServerConfig {
/** Store to load indexes from */
store: IndexStoreReader;
/** Store to load indexes from (accepts both reader-only and full store) */
store: IndexStoreReader | IndexStore;
/**
* Index names to expose. If undefined, all indexes in the store are exposed.
*/
Expand All @@ -71,6 +73,13 @@ export interface MCPServerConfig {
* @default "0.1.0"
*/
version?: string;
/**
* Discovery mode flag.
* When true: use withListIndexesReference (no enum in schemas), no write tools
Copy link
Copy Markdown

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.

* When false/undefined: use withIndexList (include enum in schemas), write tools available
* @default false
*/
discovery?: boolean;
}
/**
* Create an MCP server instance.
Expand Down Expand Up @@ -104,6 +113,7 @@ export async function createMCPServer(
searchOnly: config.searchOnly,
clientUserAgent,
});

const { indexNames, indexes } = runner;
const searchOnly = !runner.hasFileOperations();
// Format index list for tool descriptions
Expand Down Expand Up @@ -146,13 +156,35 @@ export async function createMCPServer(
required?: string[];
};
};
// Tool descriptions with available indexes (from shared module)
const searchDescription = withIndexList(SEARCH_DESCRIPTION, indexListStr);
const listFilesDescription = withIndexList(LIST_FILES_DESCRIPTION, indexListStr);
const readFileDescription = withIndexList(READ_FILE_DESCRIPTION, indexListStr);

// Tool descriptions: use enum in fixed mode, reference in discovery mode
let searchDescription: string;
let listFilesDescription: string;
let readFileDescription: string;

if (config.discovery) {
// Discovery mode: use reference to list_indexes (no enum)
searchDescription = withListIndexesReference(SEARCH_DESCRIPTION);
listFilesDescription = withListIndexesReference(LIST_FILES_DESCRIPTION);
readFileDescription = withListIndexesReference(READ_FILE_DESCRIPTION);
} else {
// Fixed mode: include enum with index list
searchDescription = withIndexList(SEARCH_DESCRIPTION, indexListStr);
listFilesDescription = withIndexList(LIST_FILES_DESCRIPTION, indexListStr);
readFileDescription = withIndexList(READ_FILE_DESCRIPTION, indexListStr);
}
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools: Tool[] = [
{
name: "list_indexes",
Copy link
Copy Markdown

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.

Comment thread
richhankins marked this conversation as resolved.
description: "List all available indexes with their metadata. Call this to discover what indexes are available before using search, list_files, or read_file tools.",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "search",
description: searchDescription,
Expand All @@ -162,7 +194,7 @@ export async function createMCPServer(
index_name: {
type: "string",
description: "Name of the index to search.",
enum: indexNames,
...(config.discovery ? {} : { enum: runner.indexes.map(i => i.name) }),
},
query: {
type: "string",
Expand All @@ -189,7 +221,7 @@ export async function createMCPServer(
index_name: {
type: "string",
description: "Name of the index.",
enum: indexNames,
...(config.discovery ? {} : { enum: runner.indexes.map(i => i.name) }),
},
directory: {
type: "string",
Expand Down Expand Up @@ -220,7 +252,7 @@ export async function createMCPServer(
index_name: {
type: "string",
description: "Name of the index.",
enum: indexNames,
...(config.discovery ? {} : { enum: runner.indexes.map(i => i.name) }),
},
path: {
type: "string",
Expand Down Expand Up @@ -261,6 +293,24 @@ export async function createMCPServer(
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;

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

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." }],
Copy link
Copy Markdown

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.

};
}
const lines = indexes.map((i) =>
`- ${i.name} (${i.type}://${i.identifier}) - synced ${i.syncedAt}`
);
return {
content: [{ type: "text", text: `Available indexes:\n${lines.join("\n")}` }],
};
}

try {
const indexName = args?.index_name as string;
const client = await runner.getClient(indexName);
Expand Down
58 changes: 46 additions & 12 deletions src/clients/multi-index-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @module clients/multi-index-runner
*/

import type { IndexStoreReader } from "../stores/types.js";
import type { IndexStoreReader, IndexStore } from "../stores/types.js";
import type { Source } from "../sources/types.js";
import type { IndexStateSearchOnly } from "../core/types.js";
import { getSourceIdentifier, getResolvedRef } from "../core/types.js";
Expand All @@ -26,7 +26,7 @@ export interface IndexInfo {
/** Configuration for MultiIndexRunner */
export interface MultiIndexRunnerConfig {
/** Store to load indexes from */
store: IndexStoreReader;
store: IndexStoreReader | IndexStore;
/**
* Index names to expose. If undefined, all indexes in the store are exposed.
*/
Expand Down Expand Up @@ -88,19 +88,19 @@ export async function createSourceFromState(state: IndexStateSearchOnly): Promis
* Lazily initializes SearchClient instances as needed and caches them.
*/
export class MultiIndexRunner {
private readonly store: IndexStoreReader;
private readonly store: IndexStoreReader | IndexStore;
private readonly searchOnly: boolean;
private clientUserAgent?: string;
private readonly clientCache = new Map<string, SearchClient>();

/** Available index names */
readonly indexNames: string[];
indexNames: string[];

/** Metadata about available indexes */
readonly indexes: IndexInfo[];
indexes: IndexInfo[];

private constructor(
store: IndexStoreReader,
store: IndexStoreReader | IndexStore,
indexNames: string[],
indexes: IndexInfo[],
searchOnly: boolean,
Expand Down Expand Up @@ -130,10 +130,6 @@ export class MultiIndexRunner {
throw new Error(`Indexes not found: ${missingIndexes.join(", ")}`);
}

if (indexNames.length === 0) {
throw new Error("No indexes available in store");
}

// Load metadata for available indexes, filtering out any that fail to load
const indexes: IndexInfo[] = [];
const validIndexNames: string[] = [];
Expand Down Expand Up @@ -163,10 +159,9 @@ export class MultiIndexRunner {
return new MultiIndexRunner(store, validIndexNames, indexes, searchOnly, config.clientUserAgent);
}


/**
* Update the User-Agent string.
*
*
* Call this after receiving MCP client info to include the client name/version.
* Note: Only affects future client creations, not existing cached clients.
*/
Expand Down Expand Up @@ -205,6 +200,45 @@ export class MultiIndexRunner {
return client;
}

/**
* Refresh the list of available indexes from the store.
* Call after adding or removing indexes.
*/
async refreshIndexList(): Promise<void> {
const allIndexNames = await this.store.list();
const newIndexes: IndexInfo[] = [];
const newIndexNames: string[] = [];

for (const name of allIndexNames) {
try {
const state = await this.store.loadSearch(name);
if (state) {
newIndexNames.push(name);
newIndexes.push({
name,
type: state.source.type,
identifier: getSourceIdentifier(state.source),
ref: getResolvedRef(state.source),
syncedAt: state.source.syncedAt,
});
}
} catch {
// Skip indexes that fail to load
}
}

this.indexNames = newIndexNames;
Copy link
Copy Markdown

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.

Copy link
Copy Markdown

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.

this.indexes = newIndexes;
}

/**
* Invalidate cached SearchClient for an index.
* Call after updating an index to ensure fresh data on next access.
*/
invalidateClient(indexName: string): void {
this.clientCache.delete(indexName);
}

/** Check if file operations are enabled */
hasFileOperations(): boolean {
return !this.searchOnly;
Expand Down
1 change: 0 additions & 1 deletion src/clients/search-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ export class SearchClient {
this.context = await DirectContext.import(this.state.contextState, {
Copy link
Copy Markdown

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.

apiKey: this.apiKey,
apiUrl: this.apiUrl,
clientUserAgent: this.clientUserAgent,
});
}

Expand Down
Loading