Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
478db4a
feat(session): preserve session on transport close instead of deleting
xizhibei Jan 15, 2026
e6db304
feat: Implement ToolRegistry with comprehensive testing and lazy load…
xizhibei Jan 12, 2026
e4a6dda
feat: Enhance lazy loading with preload tools and health status logging
xizhibei Jan 14, 2026
6e9fe2d
Merge branch 'main' into feat/lazy-loading
xizhibei Jan 18, 2026
1ce8216
feat(lazy): improve lazy loading with meta-tool renaming and on-deman…
xizhibei Jan 20, 2026
5129404
feat: update meta-tool names for consistency and add description extr…
xizhibei Jan 20, 2026
1b8ae43
feat: enhance error handling and instruction management in meta-tools…
xizhibei Jan 20, 2026
044c1e3
feat: add integration tests for lazy loading with preset/tag filtering
xizhibei Jan 22, 2026
7448223
fix: resolve critical issues in lazy loading implementation (PR #221)
xizhibei Jan 23, 2026
4abe31e
feat: add comprehensive MCP annotations to internal tools
xizhibei Jan 23, 2026
186291a
fix: resolve template server connection keys in meta-tools
xizhibei Jan 23, 2026
104725f
feat: Implement ServerRegistry and TemplateServerAdapter for managing…
xizhibei Jan 24, 2026
10084a2
Merge branch 'main' into feat/lazy-loading
xizhibei Jan 25, 2026
73e4ac7
feat: Enhance error handling and logging across various components
xizhibei Jan 25, 2026
f938901
feat: Add preload pattern validation and enhance regex handling in La…
xizhibei Jan 26, 2026
b0545ba
Merge branch 'main' into feat/lazy-loading
xizhibei Jan 28, 2026
a061e45
feat: implement pagination with default limit of 20 in ToolRegistry
xizhibei Jan 29, 2026
26e9d10
fix: resolve CI test failure and CodeRabbit review issues
xizhibei Jan 30, 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
221 changes: 164 additions & 57 deletions CHANGELOG.md

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions src/commands/serve/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,64 @@ export const serverOptions = {
type: 'boolean' as const,
default: true,
},
'enable-lazy-loading': {
describe: 'Enable lazy loading for tools (tools loaded on-demand, reduces token usage by ~95%)',
type: 'boolean' as const,
default: false,
},
'lazy-mode': {
describe: 'Lazy loading mode (metatool: 3 meta-tools only, hybrid: direct + meta-tools, full: disabled)',
type: 'string' as const,
choices: ['metatool', 'hybrid', 'full'] as const,
default: 'full',
},
'lazy-cache-max-entries': {
describe: 'Maximum number of tool schemas to cache in LRU cache (when lazy loading enabled)',
type: 'number' as const,
default: 1000,
},
'lazy-preload': {
describe: 'Comma-separated list of tool patterns to preload at startup (when lazy loading enabled)',
type: 'string' as const,
default: undefined,
},
'lazy-preload-keywords': {
describe: 'Comma-separated list of tool name keywords to preload at startup (when lazy loading enabled)',
type: 'string' as const,
default: undefined,
},
'lazy-inline-catalog': {
describe: 'Include full tool catalog in initialize template (when lazy loading enabled)',
type: 'boolean' as const,
default: false,
},
'lazy-catalog-format': {
describe: 'Format for inline tool catalog (flat, grouped, categorized)',
type: 'string' as const,
choices: ['flat', 'grouped', 'categorized'] as const,
default: 'grouped',
},
'lazy-direct-expose': {
describe: 'Comma-separated glob patterns for tools to expose directly in hybrid mode',
type: 'string' as const,
default: undefined,
},
'lazy-cache-ttl': {
describe: 'Cache TTL in milliseconds for tool schemas (optional, default: no TTL)',
type: 'number' as const,
default: undefined,
},
'lazy-fallback-on-error': {
describe: 'Behavior when on-demand loading fails (skip or full)',
type: 'string' as const,
choices: ['skip', 'full'] as const,
default: 'skip',
},
'lazy-fallback-timeout': {
describe: 'Timeout in milliseconds for on-demand schema loading',
type: 'number' as const,
default: 5000,
},
'enable-config-reload': {
describe: 'Enable automatic configuration hot-reload on file changes',
type: 'boolean' as const,
Expand Down Expand Up @@ -243,6 +301,18 @@ INTERNAL TOOLS:
• Categories: "discovery,management,installation,safe"
• Examples: "safe" (read-only), "discovery,management" (no installation)

LAZY LOADING:
Use --enable-lazy-loading to enable lazy tool loading for token optimization.
Lazy loading reduces initial token usage by ~95% by loading tools on-demand.
Modes:
• metatool: Expose only 3 meta-tools (list/describe/call) - maximum savings
• hybrid: Common tools direct + meta-tools for rest - balanced approach
• full: Disabled, load all tools normally (default, backward compatible)
Examples:
• --enable-lazy-loading --lazy-mode=metatool
• --enable-lazy-loading --lazy-mode=hybrid --lazy-preload=filesystem,search
• --enable-lazy-loading --lazy-cache-max-entries=2000

For more information: https://github.com/1mcp-app/agent
`);
},
Expand Down
30 changes: 30 additions & 0 deletions src/commands/serve/serve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ describe('serveCommand - config-dir session isolation', () => {
'async-batch-notifications': true,
'async-batch-delay': 100,
'async-notify-on-ready': true,
'enable-lazy-loading': false,
'lazy-mode': 'full',
'lazy-inline-catalog': false,
'lazy-catalog-format': 'grouped',
'lazy-cache-max-entries': 1000,
'lazy-cache-ttl': 300000,
'lazy-preload': undefined,
'lazy-preload-keywords': undefined,
'lazy-fallback-on-error': undefined,
'lazy-fallback-timeout': undefined,
'enable-config-reload': true,
'config-reload-debounce': 500,
'enable-env-substitution': true,
Expand Down Expand Up @@ -171,6 +181,16 @@ describe('serveCommand - config-dir session isolation', () => {
'async-batch-notifications': true,
'async-batch-delay': 100,
'async-notify-on-ready': true,
'enable-lazy-loading': false,
'lazy-mode': 'full',
'lazy-inline-catalog': false,
'lazy-catalog-format': 'grouped',
'lazy-cache-max-entries': 1000,
'lazy-cache-ttl': 300000,
'lazy-preload': undefined,
'lazy-preload-keywords': undefined,
'lazy-fallback-on-error': undefined,
'lazy-fallback-timeout': undefined,
'enable-config-reload': true,
'config-reload-debounce': 500,
'enable-env-substitution': true,
Expand Down Expand Up @@ -226,6 +246,16 @@ describe('serveCommand - config-dir session isolation', () => {
'async-batch-notifications': true,
'async-batch-delay': 100,
'async-notify-on-ready': true,
'enable-lazy-loading': false,
'lazy-mode': 'full',
'lazy-inline-catalog': false,
'lazy-catalog-format': 'grouped',
'lazy-cache-max-entries': 1000,
'lazy-cache-ttl': 300000,
'lazy-preload': undefined,
'lazy-preload-keywords': undefined,
'lazy-fallback-on-error': undefined,
'lazy-fallback-timeout': undefined,
'enable-config-reload': true,
'config-reload-debounce': 500,
'enable-env-substitution': true,
Expand Down
91 changes: 89 additions & 2 deletions src/commands/serve/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { AgentConfigManager } from '@src/core/server/agentConfig.js';
import { cleanupPidFileOnExit, registerPidFileCleanup, writePidFile } from '@src/core/server/pidFileManager.js';
import { ServerManager } from '@src/core/server/serverManager.js';
import { TagExpression, TagQueryParser } from '@src/domains/preset/parsers/tagQueryParser.js';
import type { TagQuery } from '@src/domains/preset/types/presetTypes.js';
import logger, { debugIf } from '@src/logger/logger.js';
import { setupServer } from '@src/server.js';
import { ExpressServer } from '@src/transport/http/server.js';
Expand Down Expand Up @@ -48,6 +49,17 @@ export interface ServeOptions {
'async-batch-notifications': boolean;
'async-batch-delay': number;
'async-notify-on-ready': boolean;
'enable-lazy-loading': boolean;
'lazy-mode': string;
'lazy-inline-catalog': boolean;
'lazy-catalog-format': string;
'lazy-direct-expose'?: string;
'lazy-cache-max-entries': number;
'lazy-cache-ttl'?: number;
'lazy-preload'?: string;
'lazy-preload-keywords'?: string;
'lazy-fallback-on-error'?: string;
'lazy-fallback-timeout'?: number;
'enable-config-reload': boolean;
'config-reload-debounce': number;
'enable-env-substitution': boolean;
Expand Down Expand Up @@ -337,6 +349,29 @@ export async function serveCommand(parsedArgv: ServeOptions): Promise<void> {
batchNotifications: parsedArgv['async-batch-notifications'],
batchDelayMs: parsedArgv['async-batch-delay'],
},
lazyLoading: {
enabled: parsedArgv['enable-lazy-loading'],
inlineCatalog: parsedArgv['lazy-inline-catalog'],
catalogFormat: (parsedArgv['lazy-catalog-format'] || 'grouped') as 'flat' | 'grouped' | 'categorized',
directExpose: parsedArgv['lazy-direct-expose']
? parsedArgv['lazy-direct-expose'].split(',').map((s) => s.trim())
: [],
cache: {
maxEntries: parsedArgv['lazy-cache-max-entries'],
strategy: 'lru' as const,
ttlMs: parsedArgv['lazy-cache-ttl'],
},
preload: {
patterns: parsedArgv['lazy-preload'] ? parsedArgv['lazy-preload'].split(',').map((s) => s.trim()) : [],
keywords: parsedArgv['lazy-preload-keywords']
? parsedArgv['lazy-preload-keywords'].split(',').map((s) => s.trim())
: [],
},
fallback: {
onError: (parsedArgv['lazy-fallback-on-error'] || 'skip') as 'skip' | 'full',
timeoutMs: parsedArgv['lazy-fallback-timeout'] ?? 30000,
},
},
configReload: {
debounceMs: parsedArgv['config-reload-debounce'],
},
Expand Down Expand Up @@ -378,9 +413,59 @@ export async function serveCommand(parsedArgv: ServeOptions): Promise<void> {
// Parse and validate filter from CLI if provided
let tags: string[] | undefined;
let tagExpression: TagExpression | undefined;
let tagFilterMode: 'simple-or' | 'advanced' | 'none' = 'none';
let tagQuery: TagQuery | undefined;
let tagFilterMode: 'simple-or' | 'advanced' | 'preset' | 'none' = 'none';
let presetName: string | undefined;

// Check for preset environment variable (for STDIO transport)
const presetEnv = process.env.ONE_MCP_PRESET;
if (presetEnv) {
const PresetManager = (await import('@src/domains/preset/manager/presetManager.js')).PresetManager;
const presetManager = PresetManager.getInstance(parsedArgv['config-dir']);

// Load presets synchronously for STDIO transport (skip file watching to avoid blocking)
try {
await presetManager.loadPresetsWithoutWatcher();
} catch (error) {
logger.warn(`Failed to load presets: ${error instanceof Error ? error.message : 'Unknown error'}`);
}

// Load presets if not already loaded
if (presetManager.hasPreset(presetEnv)) {
const preset = presetManager.getPreset(presetEnv);
if (preset) {
presetName = presetEnv;
tagQuery = preset.tagQuery;
tagFilterMode = 'preset';

// Convert tagQuery to tagExpression for compatibility
try {
// Convert JSON query to expression string first
const queryStr = presetManager.resolvePresetToExpression(presetEnv);
if (queryStr) {
tagExpression = TagQueryParser.parseAdvanced(queryStr);
// Provide simple tags for backward compat where possible
if (tagExpression?.type === 'tag') {
tags = [tagExpression.value!];
}
}
logger.info(`Loaded preset '${presetEnv}' for STDIO transport`, {
strategy: preset.strategy,
tagQuery,
});
} catch (error) {
logger.warn(
`Failed to parse preset tag query as expression: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
}
} else {
logger.warn(`Preset '${presetEnv}' not found, ignoring preset environment variable`);
}
}

if (parsedArgv.filter) {
// Fall back to CLI filter if no preset was loaded
if (!presetName && parsedArgv.filter) {
try {
// First try to parse as advanced expression
tagExpression = TagQueryParser.parseAdvanced(parsedArgv.filter);
Expand Down Expand Up @@ -415,7 +500,9 @@ export async function serveCommand(parsedArgv: ServeOptions): Promise<void> {
await serverManager.connectTransport(transport, 'stdio', {
tags,
tagExpression,
tagQuery,
tagFilterMode,
presetName,
enablePagination: parsedArgv.pagination,
customTemplate,
});
Expand Down
4 changes: 4 additions & 0 deletions src/core/capabilities/capabilityAggregator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ describe('CapabilityAggregator', () => {
listTools: vi.fn().mockResolvedValue({ tools: [mockTool] }),
listResources: vi.fn().mockResolvedValue({ resources: [mockResource] }),
listPrompts: vi.fn().mockResolvedValue({ prompts: [mockPrompt] }),
getServerCapabilities: vi.fn().mockReturnValue({ resources: true, prompts: true }),
transport: {
start: vi.fn(),
send: vi.fn(),
Expand Down Expand Up @@ -154,6 +155,7 @@ describe('CapabilityAggregator', () => {
listTools: vi.fn().mockResolvedValue({ tools: [mockTool] }),
listResources: vi.fn().mockResolvedValue({ resources: [] }),
listPrompts: vi.fn().mockResolvedValue({ prompts: [] }),
getServerCapabilities: vi.fn().mockReturnValue({ resources: true, prompts: true }),
transport: {
start: vi.fn(),
send: vi.fn(),
Expand All @@ -165,6 +167,7 @@ describe('CapabilityAggregator', () => {
listTools: vi.fn().mockResolvedValue({ tools: [duplicateTool] }),
listResources: vi.fn().mockResolvedValue({ resources: [] }),
listPrompts: vi.fn().mockResolvedValue({ prompts: [] }),
getServerCapabilities: vi.fn().mockReturnValue({ resources: true, prompts: true }),
transport: {
start: vi.fn(),
send: vi.fn(),
Expand Down Expand Up @@ -214,6 +217,7 @@ describe('CapabilityAggregator', () => {
listTools: vi.fn().mockResolvedValue({ tools: [mockTool] }),
listResources: vi.fn().mockResolvedValue({ resources: [mockResource] }),
listPrompts: vi.fn().mockResolvedValue({ prompts: [mockPrompt] }),
getServerCapabilities: vi.fn().mockReturnValue({ resources: true, prompts: true }),
transport: {
start: vi.fn(),
send: vi.fn(),
Expand Down
65 changes: 46 additions & 19 deletions src/core/capabilities/capabilityAggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,26 +168,53 @@ export class CapabilityAggregator extends EventEmitter {
try {
readyServers.push(serverName);

// Fetch tools, resources, and prompts in parallel
const [toolsResult, resourcesResult, promptsResult] = await Promise.allSettled([
this.safeListTools(serverName, connection.client),
this.safeListResources(serverName, connection.client),
this.safeListPrompts(serverName, connection.client),
]);

// Process tools
if (toolsResult.status === 'fulfilled' && toolsResult.value.tools) {
allTools.push(...toolsResult.value.tools);
// Get server capabilities to check what's supported
const serverCapabilities = connection.client.getServerCapabilities() || {};

// Build promises array based on actual capabilities
const promises: Promise<unknown>[] = [this.safeListTools(serverName, connection.client)];

if (serverCapabilities.resources) {
promises.push(this.safeListResources(serverName, connection.client));
}
if (serverCapabilities.prompts) {
promises.push(this.safeListPrompts(serverName, connection.client));
}

// Fetch capabilities in parallel (only those supported)
const results = await Promise.allSettled(promises);

// Process tools (always first in promises array)
if (results[0]?.status === 'fulfilled') {
const toolsResult = results[0].value as ListToolsResult;
if (toolsResult.tools) {
allTools.push(...toolsResult.tools);
}
}

// Process resources
if (resourcesResult.status === 'fulfilled' && resourcesResult.value.resources) {
allResources.push(...resourcesResult.value.resources);
// Process resources (second if available)
let resultIndex = 1;
if (serverCapabilities.resources) {
const resourceResult = results[resultIndex];
if (resourceResult && resourceResult.status === 'fulfilled') {
const resourcesResult = resourceResult.value as ListResourcesResult;
if (resourcesResult.resources) {
allResources.push(...resourcesResult.resources);
}
}
// Always increment index when resources capability exists, regardless of fulfillment
resultIndex++;
}

// Process prompts
if (promptsResult.status === 'fulfilled' && promptsResult.value.prompts) {
allPrompts.push(...promptsResult.value.prompts);
// Process prompts (third if available)
if (serverCapabilities.prompts) {
const promptResult = results[resultIndex];
if (promptResult && promptResult.status === 'fulfilled') {
const promptsResult = promptResult.value as ListPromptsResult;
if (promptsResult.prompts) {
allPrompts.push(...promptsResult.prompts);
}
}
}
} catch (error) {
logger.warn(`Failed to aggregate capabilities from ${serverName}: ${error}`);
Expand All @@ -214,7 +241,7 @@ export class CapabilityAggregator extends EventEmitter {
try {
return await client.listTools();
} catch (error) {
debugIf(() => ({ message: `Failed to list tools from ${serverName}: ${error}` }));
logger.warn(`Failed to list tools from ${serverName}`, { error: String(error) });
return { tools: [] };
}
}
Expand All @@ -229,7 +256,7 @@ export class CapabilityAggregator extends EventEmitter {
try {
return await client.listResources();
} catch (error) {
debugIf(() => ({ message: `Failed to list resources from ${serverName}: ${error}` }));
logger.warn(`Failed to list resources from ${serverName}`, { error: String(error) });
return { resources: [] };
}
}
Expand All @@ -244,7 +271,7 @@ export class CapabilityAggregator extends EventEmitter {
try {
return await client.listPrompts();
} catch (error) {
debugIf(() => ({ message: `Failed to list prompts from ${serverName}: ${error}` }));
logger.warn(`Failed to list prompts from ${serverName}`, { error: String(error) });
return { prompts: [] };
}
}
Expand Down
Loading
Loading