Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
31 changes: 26 additions & 5 deletions docs/en/guide/advanced/fast-startup.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,21 +130,40 @@ sequenceDiagram
par Background Loading
1MCP->>Server1: Start server
Server1-->>1MCP: Ready with tools
1MCP-->>Client: listChanged notification (tools)
and
1MCP->>Server2: Start server
Server2-->>1MCP: Ready with resources
1MCP-->>Client: listChanged notification (resources)
end
1MCP->>1MCP: Publish one completed capability snapshot
1MCP-->>Client: Coalesced listChanged notifications
```

**Benefits:**

- **Progressive Discovery**: New capabilities appear in real-time
- **Batched Notifications**: Multiple changes grouped to prevent spam
- **Immediate Listener**: HTTP requests can reach 1MCP while backends load
- **Atomic Discovery**: A loading cycle publishes one snapshot after every backend reaches a terminal state
- **Batched Notifications**: Capability-change events are coalesced to prevent spam
- **Better UX**: No need to manually refresh or reconnect

The `async-min-servers` setting is a startup readiness gate. During each loading cycle, 1MCP keeps the previous Capability Snapshot visible until every configured backend is connected or marked unavailable, then publishes the completed snapshot atomically. Inspection endpoints such as `/api/tools` query the Capability Catalog without forcing all servers to reconnect or refresh.
The HTTP listener starts accepting requests immediately. During each loading cycle, 1MCP keeps the previous Capability Snapshot visible until every configured backend is connected or marked unavailable, then publishes the completed snapshot atomically. Inspection endpoints such as `/api/tools` query the Capability Catalog without forcing all servers to reconnect or refresh.

### Async loading options

| CLI option | Default | Purpose |
| ---------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--async-batch-notifications` | Enabled | Coalesces capability-change events into fewer `listChanged` notifications. Use `--no-async-batch-notifications` to send each published change immediately. |
| `--async-batch-delay <milliseconds>` | `100` | Coalescing window used when notification batching is enabled. |
| `--async-notify-on-snapshot` | Enabled | Sends notifications when a completed loading cycle publishes a changed capability snapshot. Global `--enable-client-notifications` must also remain enabled. |
| `--async-notify-on-ready` | — | Deprecated compatibility alias for `--async-notify-on-snapshot`. If both are supplied, `--async-notify-on-snapshot` wins. |
| `--async-min-servers`, `--async-timeout` | — | Deprecated compatibility no-ops. Their `ONE_MCP_*` environment variables and `asyncLoading.minServers` / `asyncLoading.timeout` TOML keys warn when explicitly supplied and will be removed next major. |

A longer batch delay reduces notification bursts at the cost of delaying notifications after a changed snapshot is published. It does not delay listener availability or create a readiness gate.

```bash
npx -y @1mcp/agent --config mcp.json --enable-async-loading \
--async-notify-on-snapshot \
--async-batch-delay 250
```

## Configuration

Expand All @@ -156,6 +175,8 @@ For a complete list of options, see the **[Configuration Deep Dive](/guide/essen

Check the status of your MCP servers through simple HTTP endpoints.

`/health/ready` reports configuration readiness; it does not wait for a minimum number of connected backends. Use `/health/mcp` for backend loading progress and terminal states.

### Overall Status: `/health/mcp`

Get a complete overview of all your servers:
Expand Down
2 changes: 1 addition & 1 deletion docs/en/reference/health-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ X-Uptime-Seconds: 3600

### `GET /health/ready`

**Description**: Readiness probe to determine if the service is ready to accept requests.
**Description**: Configuration-readiness probe. It does not wait for backend loading or a server-count threshold; use `/health/mcp` to inspect backend loading progress.
Comment thread
adity982 marked this conversation as resolved.
Outdated

**Authentication**: None required

Expand Down
31 changes: 26 additions & 5 deletions docs/zh/guide/advanced/fast-startup.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,21 +124,40 @@ sequenceDiagram
par 后台加载
1MCP->>服务器1: 启动服务器
服务器1-->>1MCP: 服务器就绪,提供工具
1MCP-->>客户端: listChanged 通知(工具)
and
1MCP->>服务器2: 启动服务器
服务器2-->>1MCP: 服务器就绪,提供资源
1MCP-->>客户端: listChanged 通知(资源)
end
1MCP->>1MCP: 发布一个完整的能力快照
1MCP-->>客户端: 合并后的 listChanged 通知
```

**好处:**

- **渐进式发现**:新功能实时出现
- **批量通知**:将多个更改分组以防止垃圾邮件
- **监听器立即可用**:后端加载期间,HTTP 请求仍可到达 1MCP
- **原子发现**:每个加载周期会在所有后端达到终态后发布一个能力快照
- **批量通知**:合并能力变更事件,避免通知突发
- **更好的用户体验**:无需手动刷新或重新连接

`async-min-servers` 设置是启动就绪门槛。在每个加载周期中,1MCP 会继续提供上一个能力快照 (Capability Snapshot),直到所有已配置的后端均已连接或标记为不可用,再以原子方式发布完整快照。`/api/tools` 等检查端点会查询能力目录 (Capability Catalog),而不会强制所有服务器重新连接或刷新。
HTTP 监听器会立即开始接受请求。在每个加载周期中,1MCP 会继续提供上一个能力快照 (Capability Snapshot),直到所有已配置的后端均已连接或标记为不可用,再以原子方式发布完整快照。`/api/tools` 等检查端点会查询能力目录 (Capability Catalog),而不会强制所有服务器重新连接或刷新。

### 异步加载选项

| CLI 选项 | 默认值 | 用途 |
| ---------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--async-batch-notifications` | 启用 | 在批处理延迟窗口内合并能力变更事件,减少 `listChanged` 通知。使用 `--no-async-batch-notifications` 可立即发送每个已发布的变更。 |
| `--async-batch-delay <毫秒>` | `100` | 启用通知批处理时的合并窗口。 |
| `--async-notify-on-snapshot` | 启用 | 完成的加载周期发布有变化的能力快照时发送通知;全局 `--enable-client-notifications` 也必须保持启用。 |
| `--async-notify-on-ready` | — | `--async-notify-on-snapshot` 的弃用兼容别名。若同时提供,以 `--async-notify-on-snapshot` 为准。 |
| `--async-min-servers`、`--async-timeout` | — | 弃用的兼容空操作。显式使用对应的 `ONE_MCP_*` 环境变量或 `asyncLoading.minServers` / `asyncLoading.timeout` TOML 键时会发出警告,并将在下一主版本中移除。 |

较长的批处理延迟可以减少通知突发,但会推迟能力快照发布后的通知;它不会延迟监听器可用性,也不会创建就绪门槛。

```bash
npx -y @1mcp/agent --config mcp.json --enable-async-loading \
--async-notify-on-snapshot \
--async-batch-delay 250
```

## 配置

Expand All @@ -150,6 +169,8 @@ sequenceDiagram

通过简单的 HTTP 端点检查您的 MCP 服务器的状态。

`/health/ready` 表示配置是否就绪,不会等待达到最少后端连接数。后端加载进度和终态请查看 `/health/mcp`。

### 整体状态:`/health/mcp`

获取所有服务器的完整概览:
Expand Down
2 changes: 1 addition & 1 deletion docs/zh/reference/health-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ X-Uptime-Seconds: 3600

### `GET /health/ready`

**描述**:就绪性探针,用于确定服务是否已准备好接受请求
**描述**:配置就绪探针。它不会等待后端加载或达到服务器数量门槛;后端加载进度请查看 `/health/mcp`

**身份验证**:无需

Expand Down
67 changes: 67 additions & 0 deletions src/commands/serve/asyncLoadingOptions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it, vi } from 'vitest';

import { resolveAsyncLoadingOptions } from './asyncLoadingOptions.js';

describe('resolveAsyncLoadingOptions', () => {
it('uses snapshot notification defaults without deprecation warnings', () => {
const warn = vi.fn();

const result = resolveAsyncLoadingOptions({}, undefined, warn);

expect(result).toMatchObject({
enabled: false,
notifyOnServerReady: true,
waitForMinimumServers: 0,
initialLoadTimeoutMs: 30000,
});
expect(warn).not.toHaveBeenCalled();
});

it('treats deprecated CLI or environment thresholds as warning-only no-ops', () => {
const warn = vi.fn();

const result = resolveAsyncLoadingOptions({ 'async-min-servers': 12, 'async-timeout': 1 }, undefined, warn);

expect(result.waitForMinimumServers).toBe(0);
expect(result.initialLoadTimeoutMs).toBe(30000);
expect(warn).toHaveBeenCalledTimes(2);
expect(warn.mock.calls.join('\n')).toContain('ONE_MCP_ASYNC_MIN_SERVERS');
expect(warn.mock.calls.join('\n')).toContain('ONE_MCP_ASYNC_TIMEOUT');
});

it('warns for deprecated TOML thresholds without consuming their values', () => {
const warn = vi.fn();

const result = resolveAsyncLoadingOptions({}, { minServers: 4, timeout: 9000 }, warn);

expect(result.waitForMinimumServers).toBe(0);
expect(result.initialLoadTimeoutMs).toBe(30000);
expect(warn.mock.calls.join('\n')).toContain('asyncLoading.minServers');
expect(warn.mock.calls.join('\n')).toContain('asyncLoading.timeout');
});

it('supports the deprecated notification alias with a warning', () => {
const warn = vi.fn();

const result = resolveAsyncLoadingOptions({ 'async-notify-on-ready': false }, undefined, warn);

expect(result.notifyOnServerReady).toBe(false);
expect(warn).toHaveBeenCalledOnce();
});

it('gives the canonical snapshot flag precedence over the deprecated alias', () => {
const warn = vi.fn();

const result = resolveAsyncLoadingOptions(
{
'async-notify-on-snapshot': false,
'async-notify-on-ready': true,
},
undefined,
warn,
);

expect(result.notifyOnServerReady).toBe(false);
expect(warn).toHaveBeenCalledOnce();
});
});
65 changes: 65 additions & 0 deletions src/commands/serve/asyncLoadingOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
export interface AsyncLoadingCliOptions {
'enable-async-loading'?: boolean;
'async-min-servers'?: number;
'async-timeout'?: number;
'async-batch-notifications'?: boolean;
'async-batch-delay'?: number;
'async-notify-on-snapshot'?: boolean;
'async-notify-on-ready'?: boolean;
}

export interface AsyncLoadingAppOptions {
enabled?: boolean;
minServers?: number;
timeout?: number;
batchNotifications?: boolean;
batchDelay?: number;
}

export interface ResolvedAsyncLoadingOptions {
enabled: boolean;
notifyOnServerReady: boolean;
waitForMinimumServers: number;
initialLoadTimeoutMs: number;
batchNotifications: boolean;
batchDelayMs: number;
}

const DEPRECATED_INPUTS = {
minServers:
'`--async-min-servers`, `ONE_MCP_ASYNC_MIN_SERVERS`, and `asyncLoading.minServers` are deprecated compatibility no-ops',
timeout: '`--async-timeout`, `ONE_MCP_ASYNC_TIMEOUT`, and `asyncLoading.timeout` are deprecated compatibility no-ops',
} as const;

/**
* Resolve async-loading controls without treating compatibility settings as
* readiness gates. Yargs has already merged ONE_MCP_* values into cliOptions.
*/
export function resolveAsyncLoadingOptions(
cliOptions: AsyncLoadingCliOptions,
appOptions: AsyncLoadingAppOptions | undefined,
warn: (message: string) => void,
): ResolvedAsyncLoadingOptions {
if (cliOptions['async-min-servers'] !== undefined || appOptions?.minServers !== undefined) {
warn(`DEPRECATION WARNING: ${DEPRECATED_INPUTS.minServers}; remove this setting before the next breaking release.`);
}
if (cliOptions['async-timeout'] !== undefined || appOptions?.timeout !== undefined) {
warn(`DEPRECATION WARNING: ${DEPRECATED_INPUTS.timeout}; remove this setting before the next breaking release.`);
}
if (cliOptions['async-notify-on-ready'] !== undefined) {
warn(
'DEPRECATION WARNING: `--async-notify-on-ready` / `ONE_MCP_ASYNC_NOTIFY_ON_READY` is deprecated; use `--async-notify-on-snapshot` instead.',
);
}

return {
enabled: cliOptions['enable-async-loading'] ?? appOptions?.enabled ?? false,
// The canonical spelling wins when both it and the compatibility alias are explicit.
notifyOnServerReady: cliOptions['async-notify-on-snapshot'] ?? cliOptions['async-notify-on-ready'] ?? true,
// Retained internal fields are deliberately fixed: neither is a readiness gate.
waitForMinimumServers: 0,
initialLoadTimeoutMs: 30000,
batchNotifications: cliOptions['async-batch-notifications'] ?? appOptions?.batchNotifications ?? true,
batchDelayMs: cliOptions['async-batch-delay'] ?? appOptions?.batchDelay ?? 100,
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
24 changes: 24 additions & 0 deletions src/commands/serve/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,28 @@ describe('setupServeCommand', () => {

expect(serveCommandMock).toHaveBeenCalledWith(expect.objectContaining({ preset: 'production' }));
});

it('passes the canonical async snapshot notification flag through', async () => {
await setupServeCommand(yargs([]).exitProcess(false).help(false).version(false)).parseAsync([
'serve',
'--no-async-notify-on-snapshot',
]);

expect(serveCommandMock).toHaveBeenCalledWith(expect.objectContaining({ 'async-notify-on-snapshot': false }));
});

it('passes deprecated async environment inputs through for warning handling', async () => {
const previousValue = process.env.ONE_MCP_ASYNC_MIN_SERVERS;
process.env.ONE_MCP_ASYNC_MIN_SERVERS = '7';
try {
await setupServeCommand(yargs([]).env('ONE_MCP').exitProcess(false).help(false).version(false)).parseAsync([
'serve',
]);

expect(serveCommandMock).toHaveBeenCalledWith(expect.objectContaining({ 'async-min-servers': 7 }));
} finally {
if (previousValue === undefined) delete process.env.ONE_MCP_ASYNC_MIN_SERVERS;
else process.env.ONE_MCP_ASYNC_MIN_SERVERS = previousValue;
}
});
Comment thread
adity982 marked this conversation as resolved.
});
13 changes: 8 additions & 5 deletions src/commands/serve/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,25 +138,28 @@ export const serverOptions = {
type: 'boolean' as const,
},
'async-min-servers': {
describe: 'Minimum number of servers to wait for before accepting requests (when async loading enabled)',
describe: 'Deprecated compatibility no-op; the listener does not wait for a server-count threshold',
type: 'number' as const,
},
'async-timeout': {
describe: 'Initial load timeout in milliseconds (when async loading enabled)',
describe: 'Deprecated compatibility no-op; async loading does not create a startup readiness timeout',
type: 'number' as const,
},
'async-batch-notifications': {
describe: 'Batch capability change notifications (when async loading enabled)',
describe: 'Coalesce capability-change notifications within the async batch delay window',
type: 'boolean' as const,
},
'async-batch-delay': {
describe: 'Batch delay in milliseconds for notifications (when async loading enabled)',
type: 'number' as const,
},
'async-notify-on-snapshot': {
describe: 'Notify clients when a completed loading cycle publishes a changed capability snapshot',
type: 'boolean' as const,
},
'async-notify-on-ready': {
describe: 'Notify clients when servers become ready (when async loading enabled)',
describe: 'Deprecated alias for --async-notify-on-snapshot',
type: 'boolean' as const,
default: true,
},
'enable-lazy-loading': {
describe: 'Enable lazy loading for tools (tools loaded on-demand, reduces token usage by ~95%)',
Expand Down
4 changes: 2 additions & 2 deletions src/commands/serve/serve.transport-precedence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,8 +290,8 @@ describe('serveCommand - config-dir session isolation', () => {
}),
asyncLoading: expect.objectContaining({
enabled: true,
waitForMinimumServers: 3,
initialLoadTimeoutMs: 1234,
waitForMinimumServers: 0,
initialLoadTimeoutMs: 30000,
batchNotifications: false,
batchDelayMs: 55,
}),
Expand Down
17 changes: 7 additions & 10 deletions src/commands/serve/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { setupServer } from '@src/server.js';
import { ExpressServer } from '@src/transport/http/server.js';
import { displayLogo } from '@src/utils/ui/logo.js';

import { resolveAsyncLoadingOptions } from './asyncLoadingOptions.js';
import { resolveServeConfigPaths } from './runtimeScope.js';
import { parseCommaSeparatedList, parseInternalToolsList, resolveStdioFilterConfig } from './serveOptions.js';

Expand Down Expand Up @@ -71,7 +72,8 @@ export interface ServeOptions {
'async-timeout'?: number;
'async-batch-notifications'?: boolean;
'async-batch-delay'?: number;
'async-notify-on-ready': boolean;
'async-notify-on-snapshot'?: boolean;
'async-notify-on-ready'?: boolean;
'enable-lazy-loading'?: boolean;
'lazy-mode'?: string;
'lazy-inline-catalog'?: boolean;
Expand Down Expand Up @@ -440,6 +442,9 @@ export async function serveCommand(parsedArgv: ServeOptions): Promise<void> {
const preloadPatterns = parseCommaSeparatedList(parsedArgv['lazy-preload']);
const preloadKeywords = parseCommaSeparatedList(parsedArgv['lazy-preload-keywords']);
const sessionTtlMinutes = parsedArgv['session-ttl'] ?? appConfig.auth?.sessionTtl ?? 1440;
const asyncLoading = resolveAsyncLoadingOptions(parsedArgv, appConfig.asyncLoading, (warning) =>
logger.warn(warning),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

serverConfigManager.updateConfig({
host: effectiveHost,
Expand Down Expand Up @@ -477,15 +482,7 @@ export async function serveCommand(parsedArgv: ServeOptions): Promise<void> {
health: {
detailLevel: parsedArgv['health-info-level'] as 'full' | 'basic' | 'minimal',
},
asyncLoading: {
enabled: parsedArgv['enable-async-loading'] ?? appConfig.asyncLoading?.enabled ?? false,
notifyOnServerReady: parsedArgv['async-notify-on-ready'],
waitForMinimumServers: parsedArgv['async-min-servers'] ?? appConfig.asyncLoading?.minServers ?? 1,
initialLoadTimeoutMs: parsedArgv['async-timeout'] ?? appConfig.asyncLoading?.timeout ?? 30000,
batchNotifications:
parsedArgv['async-batch-notifications'] ?? appConfig.asyncLoading?.batchNotifications ?? true,
batchDelayMs: parsedArgv['async-batch-delay'] ?? appConfig.asyncLoading?.batchDelay ?? 100,
},
asyncLoading,
lazyLoading: {
enabled: parsedArgv['enable-lazy-loading'] ?? appConfig.lazyLoading?.enabled ?? false,
inlineCatalog: parsedArgv['lazy-inline-catalog'] ?? appConfig.lazyLoading?.inlineCatalog ?? false,
Expand Down
1 change: 1 addition & 0 deletions src/commands/serve/serveBackground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const BACKGROUND_STARTUP_OPTION_KEYS = [
'async-timeout',
'async-batch-notifications',
'async-batch-delay',
'async-notify-on-snapshot',
'async-notify-on-ready',
'enable-lazy-loading',
'lazy-mode',
Expand Down
Loading