Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,17 @@ flowchart LR
H --> B
```

1MCP runs as an aggregated runtime behind `1mcp serve`. Static servers are prepared from startup configuration, template servers are materialized when client context is known, and the runtime can use async loading and lazy loading to reduce startup blocking and tool-surface noise. Instruction aggregation, presets, and notifications sit alongside that runtime rather than outside it.
1MCP runs as an aggregated runtime behind `1mcp serve`. Static servers are prepared from startup configuration, template servers are materialized when client context is known, and async loading can reduce startup blocking. Instruction aggregation, presets, and notifications sit alongside that runtime rather than outside it.

Lazy loading is an opt-in stable tool-surface compatibility mode. It keeps the backend discovery and invocation surface at `tool_list`, `tool_schema`, and `tool_invoke` so capable agents can discover tools progressively without replacing their MCP tool table. Any explicitly enabled internal management tools remain directly exposed. Lazy loading reduces the initial schema payload, but it does not reduce backend connections or processes, make synchronous startup bind earlier, or repair orphaned proxy processes. See [#392](https://github.com/1mcp-app/agent/issues/392) for the async late-server visibility contract.

## Core Capabilities

- Unified runtime for many MCP servers behind one `serve` process
- CLI mode for progressive discovery with `1mcp instructions`, `1mcp inspect <server>`, `1mcp inspect <server>/<tool>`, and `1mcp run <server>/<tool> --args '<json>'`
- Template servers for per-client or per-session resolution
- Async loading and lazy loading for faster startup and narrower exposure
- Async loading for earlier HTTP availability while static servers load
- Opt-in lazy loading for a stable progressive-discovery tool surface and smaller initial schemas
- Opt-in automatic recovery for owned stdio backends, with health/status visibility and operator restart controls
- Instruction aggregation across static and template-backed servers
- Presets, filters, and preset change notifications
Expand Down
6 changes: 2 additions & 4 deletions config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,10 @@ timeout = 30000
# batchDelay = 100

[lazyLoading]
# Enable lazy loading for tools
# Opt in to stable meta-tool exposure for progressive discovery. When false
# (the default), all tools are exposed directly.
enabled = false

# Lazy loading mode: "full" | "metatool" | "hybrid"
mode = "full"

# Maximum tool schemas to cache
# cacheMaxEntries = 1000

Expand Down
6 changes: 3 additions & 3 deletions docs/en/commands/serve.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ For runtime-wide configuration details, see the **[Configuration Guide](/guide/e
### Runtime behavior

- **`--enable-async-loading`**: Start HTTP availability before all static servers finish loading.
- **`--enable-lazy-loading`**: Enable lazy loading behavior for exposed server capabilities.
- **`--enable-lazy-loading`**: Opt into meta-tool exposure for progressive tool discovery. Omit it for full direct exposure.
- **`--enable-config-reload`**: Enable config reload handling.
- **`--enable-session-persistence`**: Enable HTTP session persistence.

Expand Down Expand Up @@ -234,10 +234,10 @@ Behavior:
1mcp serve --config-dir ./config
```

### Start with async and lazy loading
### Start with lazy loading

```bash
1mcp serve --enable-async-loading --enable-lazy-loading
1mcp serve --enable-lazy-loading
```

### Start with filtered server exposure
Expand Down
21 changes: 21 additions & 0 deletions docs/en/guide/essentials/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,27 @@ ONE_MCP_PAGINATION=true \
npx -y @1mcp/agent
```

### Lazy Loading

Lazy loading is opt-in stable tool-surface compatibility for clients that support progressive discovery. With it disabled (the default), 1MCP exposes every backend tool directly. With it enabled, the backend tool surface is `tool_list`, `tool_schema`, and `tool_invoke`; clients discover schemas and invoke backend tools through those meta-tools. Internal management tools explicitly enabled by the operator remain directly exposed with the `1mcp` namespace.

This reduces the initial tool-schema payload. It does not reduce backend connections or processes, make synchronous startup bind earlier, or repair orphaned proxy processes. Do not enable it together with async loading by default: choose each setting for its own runtime behavior. See [#392](https://github.com/1mcp-app/agent/issues/392) for the request-time visibility contract that lets a matching late-ready server appear after async capability publication without reconnecting.

Enable it for one launch:

```bash
1mcp serve --enable-lazy-loading
```

Or make it persistent in `config.toml` and restart the process:

```toml
[lazyLoading]
enabled = true
```

The CLI flag takes precedence over `config.toml`. Legacy `--lazy-mode`, `[lazyLoading].mode`, and `--lazy-direct-expose` inputs are accepted temporarily but ignored with a deprecation warning; `enabled` is the only working switch.

### Configuration Reload

Control configuration file hot-reload behavior for seamless updates.
Expand Down
6 changes: 3 additions & 3 deletions docs/zh/commands/serve.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ CLI 模式依赖一个正在运行的 `serve` 实例。
### 运行时行为

- **`--enable-async-loading`**:让 HTTP 可用性先启动,再等待静态服务器完成加载。
- **`--enable-lazy-loading`**:为服务能力启用懒加载行为
- **`--enable-lazy-loading`**:选择使用元工具逐步发现工具;省略该选项则直接暴露全部工具
- **`--enable-config-reload`**:启用配置重载处理。
- **`--enable-session-persistence`**:启用 HTTP 会话持久化。

Expand Down Expand Up @@ -224,10 +224,10 @@ Stopped supervised background runtime in Runtime Scope /home/me/.config/1mcp (su
1mcp serve --config-dir ./config
```

### 启用异步加载与懒加载
### 启用懒加载

```bash
1mcp serve --enable-async-loading --enable-lazy-loading
1mcp serve --enable-lazy-loading
```

### 启动时筛选服务器暴露面
Expand Down
21 changes: 21 additions & 0 deletions docs/zh/guide/essentials/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,27 @@ ONE_MCP_PAGINATION=true \
npx -y @1mcp/agent
```

### 懒加载

懒加载是一种可选的稳定工具表面兼容模式,适用于支持逐步发现工具的客户端。默认关闭时,1MCP 会直接暴露全部后端工具;启用后,后端工具表面为 `tool_list`、`tool_schema` 和 `tool_invoke`,客户端通过这些元工具发现 Schema 并调用后端工具。由运维人员显式启用的内部管理工具仍会以 `1mcp` 命名空间直接暴露。

它可以减少初始工具 Schema 的负载,但不会减少后端连接或进程、让同步启动更早绑定端口,也不会修复遗留的代理进程。不要默认将它与异步加载同时启用;应根据各自的运行时行为分别选择。异步能力发布后,符合条件的后就绪服务器无需重连即可在下一次请求中可见;该请求时可见性契约见 [#392](https://github.com/1mcp-app/agent/issues/392)。

仅为本次启动启用:

```bash
1mcp serve --enable-lazy-loading
```

也可以在 `config.toml` 中持久启用,并重启进程:

```toml
[lazyLoading]
enabled = true
```

CLI 参数优先于 `config.toml`。旧的 `--lazy-mode`、`[lazyLoading].mode` 和 `--lazy-direct-expose` 输入会在一个兼容周期内继续接受,但将被忽略并产生弃用警告;`enabled` 是唯一有效的开关。

### 配置重载

控制配置文件热重载行为以实现无缝更新。
Expand Down
16 changes: 6 additions & 10 deletions src/commands/serve/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export const serverOptions = {
type: 'boolean' as const,
},
'lazy-mode': {
describe: 'Lazy loading mode (metatool: 3 meta-tools only, hybrid: direct + meta-tools, full: disabled)',
describe: 'Deprecated and ignored. Use --enable-lazy-loading to expose meta-tools, or omit it for full direct exposure.',
type: 'string' as const,
choices: ['metatool', 'hybrid', 'full'] as const,
},
Expand Down Expand Up @@ -192,7 +192,7 @@ export const serverOptions = {
default: 'grouped',
},
'lazy-direct-expose': {
describe: 'Comma-separated glob patterns for tools to expose directly in hybrid mode',
describe: 'Deprecated and ignored. Lazy loading does not support direct tool exposure.',
type: 'string' as const,
default: undefined,
},
Expand Down Expand Up @@ -338,15 +338,11 @@ INTERNAL TOOLS:
• 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)
Use --enable-lazy-loading to opt into stable meta-tool exposure for progressive discovery.
Omit it (the default) for full direct tool exposure. Legacy --lazy-mode and
--lazy-direct-expose inputs are accepted temporarily but ignored with a warning.
Examples:
• --enable-lazy-loading --lazy-mode=metatool
• --enable-lazy-loading --lazy-mode=hybrid --lazy-preload=filesystem,search
• --enable-lazy-loading
• --enable-lazy-loading --lazy-cache-max-entries=2000

For more information: https://github.com/1mcp-app/agent
Expand Down
1 change: 0 additions & 1 deletion src/commands/serve/serve.stdio-filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ function makeStdioOptions(overrides: Partial<ServeOptions> = {}): ServeOptions {
'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,
Expand Down
3 changes: 0 additions & 3 deletions src/commands/serve/serve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ describe('serveCommand - config-dir session isolation', () => {
'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,
Expand Down Expand Up @@ -208,7 +207,6 @@ describe('serveCommand - config-dir session isolation', () => {
'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,
Expand Down Expand Up @@ -272,7 +270,6 @@ describe('serveCommand - config-dir session isolation', () => {
'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,
Expand Down
26 changes: 25 additions & 1 deletion src/commands/serve/serve.transport-precedence.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AgentConfigManager } from '@src/core/server/agentConfig.js';
import { configureGlobalLogger } from '@src/logger/configureGlobalLogger.js';
import logger from '@src/logger/logger.js';
import { displayLogo } from '@src/utils/ui/logo.js';

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
Expand Down Expand Up @@ -110,7 +111,6 @@ describe('serveCommand - config-dir session isolation', () => {
'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,
Expand Down Expand Up @@ -196,6 +196,30 @@ describe('serveCommand - config-dir session isolation', () => {
);
});

it('warns for legacy lazy selectors without changing the enabled switch', async () => {
const configManager = AgentConfigManager.getInstance();
const updateConfigSpy = vi.mocked(configManager.updateConfig);

try {
await serveCommand({
...baseOptions,
transport: 'http',
port: 3050,
host: '127.0.0.1',
'lazy-mode': 'hybrid',
'lazy-direct-expose': 'filesystem_*',
});
} catch {
// Ignore errors from mocked dependencies.
}

expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('--lazy-mode is ignored'));
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('--lazy-direct-expose is ignored'));
expect(updateConfigSpy).toHaveBeenCalledWith(
expect.objectContaining({ lazyLoading: expect.objectContaining({ enabled: false }) }),
);
});

it('uses config.toml app settings when matching CLI flags are omitted', async () => {
const { ConfigManager } = await import('@src/config/configManager.js');
vi.mocked(ConfigManager.getInstance).mockReturnValue({
Expand Down
17 changes: 17 additions & 0 deletions src/commands/serve/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,22 @@ function loadInstructionsTemplate(templatePath?: string, configDir?: string): st
}
}

function warnForLegacyLazyLoadingOptions(parsedArgv: ServeOptions): void {
if (parsedArgv['lazy-mode'] !== undefined) {
logger.warn(
'DEPRECATION WARNING: --lazy-mode is ignored. Lazy loading is controlled only by --enable-lazy-loading; ' +
'for a persistent setting, use [lazyLoading] enabled = true in config.toml. Remove --lazy-mode because it does not change runtime behavior.',
);
}

if (parsedArgv['lazy-direct-expose'] !== undefined) {
logger.warn(
'DEPRECATION WARNING: --lazy-direct-expose is ignored. Lazy loading exposes only meta-tools when enabled; ' +
'for a persistent setting, use [lazyLoading] enabled = true in config.toml. Remove --lazy-direct-expose because it does not change runtime behavior.',
);
}
}

/**
* Set up graceful shutdown handling
*/
Expand Down Expand Up @@ -436,6 +452,7 @@ export async function serveCommand(parsedArgv: ServeOptions): Promise<void> {
}

const internalToolsList = parseInternalToolsList(parsedArgv['internal-tools']);
warnForLegacyLazyLoadingOptions(parsedArgv);
const directExpose = parseCommaSeparatedList(parsedArgv['lazy-direct-expose']);
const preloadPatterns = parseCommaSeparatedList(parsedArgv['lazy-preload']);
const preloadKeywords = parseCommaSeparatedList(parsedArgv['lazy-preload-keywords']);
Expand Down
17 changes: 17 additions & 0 deletions src/config/configLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,22 @@ function warnIfLegacyAppConfig(rawConfig: unknown, configFilePath: string): void
);
}

function warnForLegacyLazyLoadingConfig(rawConfig: unknown, tomlPath: string): void {
if (!rawConfig || typeof rawConfig !== 'object') {
return;
}

const lazyLoading = (rawConfig as Record<string, unknown>).lazyLoading;
if (!lazyLoading || typeof lazyLoading !== 'object' || !('mode' in lazyLoading)) {
return;
}

logger.warn(
`The [lazyLoading].mode setting in ${tomlPath} is deprecated and ignored. ` +
'Lazy loading is controlled only by [lazyLoading] enabled = true. Remove mode because it does not change runtime behavior.',
);
}

function warnForUnknownGlobalConfigKeys(rawGlobal: unknown): void {
const unknownGlobalKeys = getUnknownGlobalConfigKeys(rawGlobal);
if (unknownGlobalKeys.length === 0) {
Expand Down Expand Up @@ -118,6 +134,7 @@ export function loadAppConfigFromTomlPath(tomlPath: string): ApplicationConfig {
}
const raw = fs.readFileSync(tomlPath, 'utf8');
const parsed = parseToml(raw);
warnForLegacyLazyLoadingConfig(parsed, tomlPath);
return applicationConfigSchema.parse(parsed);
} catch (error) {
if (error instanceof ZodError) {
Expand Down
26 changes: 26 additions & 0 deletions src/config/configLoader.validation-app-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { tmpdir } from 'os';
import { join } from 'path';

import { ConfigLoader } from '@src/config/configLoader.js';
import logger from '@src/logger/logger.js';

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

Expand Down Expand Up @@ -63,6 +64,7 @@ describe('ConfigLoader', () => {
// Ignore cleanup errors
}
vi.clearAllMocks();
vi.restoreAllMocks();
resetMockAgentConfig();
});

Expand Down Expand Up @@ -417,6 +419,30 @@ minServers = 2
expect(result.asyncLoading?.minServers).toBe(2);
});

it('accepts the legacy lazy mode while warning that enabled controls runtime behavior', async () => {
const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => logger);
await fsPromises.writeFile(
join(tempConfigDir, 'config.toml'),
'[lazyLoading]\nenabled = false\nmode = "hybrid"\n',
);

const result = loader.loadAppConfigFromToml();

expect(result.lazyLoading).toEqual({ enabled: false, mode: 'hybrid' });
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('[lazyLoading].mode'));
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('enabled = true'));
});

it('does not warn when lazy loading uses only the enabled switch', async () => {
const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => logger);
await fsPromises.writeFile(join(tempConfigDir, 'config.toml'), '[lazyLoading]\nenabled = true\n');

const result = loader.loadAppConfigFromToml();

expect(result.lazyLoading).toEqual({ enabled: true });
expect(warnSpy).not.toHaveBeenCalled();
});

it('should return empty object and warn on invalid TOML', async () => {
await fsPromises.writeFile(join(tempConfigDir, 'config.toml'), 'invalid = [toml');

Expand Down
Loading