From 83a355e5aa7eb138158d8fb67f021a1acc13f4cc Mon Sep 17 00:00:00 2001 From: Aditya Datta Date: Mon, 27 Jul 2026 12:03:11 +0530 Subject: [PATCH 1/3] docs: document async loading options --- docs/en/guide/advanced/fast-startup.md | 19 +++++++++++++++++++ docs/zh/guide/advanced/fast-startup.md | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/docs/en/guide/advanced/fast-startup.md b/docs/en/guide/advanced/fast-startup.md index 94c8e00a..d64b74bc 100644 --- a/docs/en/guide/advanced/fast-startup.md +++ b/docs/en/guide/advanced/fast-startup.md @@ -146,6 +146,25 @@ sequenceDiagram 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. +### Async loading options + +| CLI option | Default | Purpose | +| ------------------------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `--async-min-servers ` | `1` | Minimum number of connected servers before the HTTP listener starts accepting requests. | +| `--async-timeout ` | `30000` | Maximum time to wait for the minimum server count before startup continues. | +| `--async-batch-notifications` | Enabled | Groups capability changes into fewer `listChanged` notifications. Use `--no-async-batch-notifications` to send each change immediately. | +| `--async-batch-delay ` | `100` | Collection window used when notification batching is enabled. | +| `--async-notify-on-ready` | Enabled | Sends capability notifications as servers become ready. Use `--no-async-notify-on-ready` only when clients refresh capabilities another way. | + +For a small fleet, the defaults are usually sufficient. For a larger fleet, raise `--async-min-servers` if clients need a useful initial set of capabilities and increase `--async-timeout` to accommodate slower backends. A longer batch delay reduces notification bursts at the cost of making newly available capabilities visible slightly later. + +```bash +npx -y @1mcp/agent --config mcp.json --enable-async-loading \ + --async-min-servers 5 \ + --async-timeout 60000 \ + --async-batch-delay 250 +``` + ## Configuration You can customize the async loading behavior, such as timeouts and retry logic, in the `loading` section of your JSON configuration file. diff --git a/docs/zh/guide/advanced/fast-startup.md b/docs/zh/guide/advanced/fast-startup.md index a1ee2fe0..b59cd1a4 100644 --- a/docs/zh/guide/advanced/fast-startup.md +++ b/docs/zh/guide/advanced/fast-startup.md @@ -140,6 +140,25 @@ sequenceDiagram `async-min-servers` 设置是启动就绪门槛。在每个加载周期中,1MCP 会继续提供上一个能力快照 (Capability Snapshot),直到所有已配置的后端均已连接或标记为不可用,再以原子方式发布完整快照。`/api/tools` 等检查端点会查询能力目录 (Capability Catalog),而不会强制所有服务器重新连接或刷新。 +### 异步加载选项 + +| CLI 选项 | 默认值 | 用途 | +| ----------------------------- | ------- | ----------------------------------------------------------------------------------------------------- | +| `--async-min-servers <数量>` | `1` | HTTP 监听器开始接受请求前所需的最少已连接服务器数量。 | +| `--async-timeout <毫秒>` | `30000` | 等待达到最少服务器数量的最长时间,超时后启动会继续。 | +| `--async-batch-notifications` | 启用 | 将能力变更合并为较少的 `listChanged` 通知。使用 `--no-async-batch-notifications` 可立即发送每项变更。 | +| `--async-batch-delay <毫秒>` | `100` | 启用通知批处理时的收集窗口。 | +| `--async-notify-on-ready` | 启用 | 在服务器就绪时发送能力通知。仅当客户端通过其他方式刷新能力时,才使用 `--no-async-notify-on-ready`。 | + +对于小型服务器组,默认值通常已经足够。对于较大的服务器组,如果客户端需要一组实用的初始能力,可以提高 `--async-min-servers`;如果后端启动较慢,可以增加 `--async-timeout`。较长的批处理延迟可以减少通知突发,但新能力的显示时间也会稍晚。 + +```bash +npx -y @1mcp/agent --config mcp.json --enable-async-loading \ + --async-min-servers 5 \ + --async-timeout 60000 \ + --async-batch-delay 250 +``` + ## 配置 您可以在 JSON 配置文件的 `loading` 部分自定义异步加载行为,例如超时和重试逻辑。 From 0249dd1c8218f71baa3459add9e84b5a6d144c8a Mon Sep 17 00:00:00 2001 From: Aditya Datta Date: Fri, 31 Jul 2026 13:51:45 +0530 Subject: [PATCH 2/3] fix(serve): align async loading options with snapshot semantics --- docs/en/guide/advanced/fast-startup.md | 32 ++++----- docs/en/reference/health-check.md | 2 +- docs/zh/guide/advanced/fast-startup.md | 32 ++++----- docs/zh/reference/health-check.md | 2 +- .../serve/asyncLoadingOptions.test.ts | 67 +++++++++++++++++++ src/commands/serve/asyncLoadingOptions.ts | 65 ++++++++++++++++++ src/commands/serve/index.test.ts | 24 +++++++ src/commands/serve/index.ts | 13 ++-- .../serve/serve.transport-precedence.test.ts | 4 +- src/commands/serve/serve.ts | 17 ++--- src/commands/serve/serveBackground.ts | 1 + .../notifications/notificationManager.test.ts | 23 +++++++ src/core/types/transport.ts | 23 +++++-- 13 files changed, 252 insertions(+), 53 deletions(-) create mode 100644 src/commands/serve/asyncLoadingOptions.test.ts create mode 100644 src/commands/serve/asyncLoadingOptions.ts diff --git a/docs/en/guide/advanced/fast-startup.md b/docs/en/guide/advanced/fast-startup.md index d64b74bc..6681f710 100644 --- a/docs/en/guide/advanced/fast-startup.md +++ b/docs/en/guide/advanced/fast-startup.md @@ -130,38 +130,38 @@ 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-min-servers ` | `1` | Minimum number of connected servers before the HTTP listener starts accepting requests. | -| `--async-timeout ` | `30000` | Maximum time to wait for the minimum server count before startup continues. | -| `--async-batch-notifications` | Enabled | Groups capability changes into fewer `listChanged` notifications. Use `--no-async-batch-notifications` to send each change immediately. | -| `--async-batch-delay ` | `100` | Collection window used when notification batching is enabled. | -| `--async-notify-on-ready` | Enabled | Sends capability notifications as servers become ready. Use `--no-async-notify-on-ready` only when clients refresh capabilities another way. | +| 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 ` | `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. | -For a small fleet, the defaults are usually sufficient. For a larger fleet, raise `--async-min-servers` if clients need a useful initial set of capabilities and increase `--async-timeout` to accommodate slower backends. A longer batch delay reduces notification bursts at the cost of making newly available capabilities visible slightly later. +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-min-servers 5 \ - --async-timeout 60000 \ + --async-notify-on-snapshot \ --async-batch-delay 250 ``` @@ -175,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: diff --git a/docs/en/reference/health-check.md b/docs/en/reference/health-check.md index bc64c104..b1cfc04f 100644 --- a/docs/en/reference/health-check.md +++ b/docs/en/reference/health-check.md @@ -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. **Authentication**: None required diff --git a/docs/zh/guide/advanced/fast-startup.md b/docs/zh/guide/advanced/fast-startup.md index b59cd1a4..6ab4e84e 100644 --- a/docs/zh/guide/advanced/fast-startup.md +++ b/docs/zh/guide/advanced/fast-startup.md @@ -124,38 +124,38 @@ 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-min-servers <数量>` | `1` | HTTP 监听器开始接受请求前所需的最少已连接服务器数量。 | -| `--async-timeout <毫秒>` | `30000` | 等待达到最少服务器数量的最长时间,超时后启动会继续。 | -| `--async-batch-notifications` | 启用 | 将能力变更合并为较少的 `listChanged` 通知。使用 `--no-async-batch-notifications` 可立即发送每项变更。 | -| `--async-batch-delay <毫秒>` | `100` | 启用通知批处理时的收集窗口。 | -| `--async-notify-on-ready` | 启用 | 在服务器就绪时发送能力通知。仅当客户端通过其他方式刷新能力时,才使用 `--no-async-notify-on-ready`。 | +| 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 键时会发出警告,并将在下一主版本中移除。 | -对于小型服务器组,默认值通常已经足够。对于较大的服务器组,如果客户端需要一组实用的初始能力,可以提高 `--async-min-servers`;如果后端启动较慢,可以增加 `--async-timeout`。较长的批处理延迟可以减少通知突发,但新能力的显示时间也会稍晚。 +较长的批处理延迟可以减少通知突发,但会推迟能力快照发布后的通知;它不会延迟监听器可用性,也不会创建就绪门槛。 ```bash npx -y @1mcp/agent --config mcp.json --enable-async-loading \ - --async-min-servers 5 \ - --async-timeout 60000 \ + --async-notify-on-snapshot \ --async-batch-delay 250 ``` @@ -169,6 +169,8 @@ npx -y @1mcp/agent --config mcp.json --enable-async-loading \ 通过简单的 HTTP 端点检查您的 MCP 服务器的状态。 +`/health/ready` 表示配置是否就绪,不会等待达到最少后端连接数。后端加载进度和终态请查看 `/health/mcp`。 + ### 整体状态:`/health/mcp` 获取所有服务器的完整概览: diff --git a/docs/zh/reference/health-check.md b/docs/zh/reference/health-check.md index 79f2a916..2253e9cb 100644 --- a/docs/zh/reference/health-check.md +++ b/docs/zh/reference/health-check.md @@ -155,7 +155,7 @@ X-Uptime-Seconds: 3600 ### `GET /health/ready` -**描述**:就绪性探针,用于确定服务是否已准备好接受请求。 +**描述**:配置就绪探针。它不会等待后端加载或达到服务器数量门槛;后端加载进度请查看 `/health/mcp`。 **身份验证**:无需 diff --git a/src/commands/serve/asyncLoadingOptions.test.ts b/src/commands/serve/asyncLoadingOptions.test.ts new file mode 100644 index 00000000..e6b1e5c9 --- /dev/null +++ b/src/commands/serve/asyncLoadingOptions.test.ts @@ -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(); + }); +}); diff --git a/src/commands/serve/asyncLoadingOptions.ts b/src/commands/serve/asyncLoadingOptions.ts new file mode 100644 index 00000000..dcc5972b --- /dev/null +++ b/src/commands/serve/asyncLoadingOptions.ts @@ -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, + }; +} diff --git a/src/commands/serve/index.test.ts b/src/commands/serve/index.test.ts index 2b6e4562..a93fefe1 100644 --- a/src/commands/serve/index.test.ts +++ b/src/commands/serve/index.test.ts @@ -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; + } + }); }); diff --git a/src/commands/serve/index.ts b/src/commands/serve/index.ts index 697197d9..17b5768c 100644 --- a/src/commands/serve/index.ts +++ b/src/commands/serve/index.ts @@ -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%)', diff --git a/src/commands/serve/serve.transport-precedence.test.ts b/src/commands/serve/serve.transport-precedence.test.ts index cc28d461..b9927d45 100644 --- a/src/commands/serve/serve.transport-precedence.test.ts +++ b/src/commands/serve/serve.transport-precedence.test.ts @@ -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, }), diff --git a/src/commands/serve/serve.ts b/src/commands/serve/serve.ts index 726cb268..cd829eac 100644 --- a/src/commands/serve/serve.ts +++ b/src/commands/serve/serve.ts @@ -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'; @@ -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; @@ -440,6 +442,9 @@ export async function serveCommand(parsedArgv: ServeOptions): Promise { 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), + ); serverConfigManager.updateConfig({ host: effectiveHost, @@ -477,15 +482,7 @@ export async function serveCommand(parsedArgv: ServeOptions): Promise { 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, diff --git a/src/commands/serve/serveBackground.ts b/src/commands/serve/serveBackground.ts index aabfc59f..d99da4ba 100644 --- a/src/commands/serve/serveBackground.ts +++ b/src/commands/serve/serveBackground.ts @@ -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', diff --git a/src/core/notifications/notificationManager.test.ts b/src/core/notifications/notificationManager.test.ts index c8ffd6bf..7f426b87 100644 --- a/src/core/notifications/notificationManager.test.ts +++ b/src/core/notifications/notificationManager.test.ts @@ -1,4 +1,5 @@ import { AggregatedCapabilities, CapabilityChanges } from '@src/core/capabilities/capabilityAggregator.js'; +import { AgentConfigManager } from '@src/core/server/agentConfig.js'; import { InboundConnection, ServerStatus } from '@src/core/types/index.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -101,6 +102,28 @@ describe('NotificationManager', () => { manager.shutdown(); }); + it('should honor the global client notification gate', () => { + vi.spyOn(AgentConfigManager, 'getInstance').mockReturnValue({ + get: vi.fn().mockReturnValue({ clientNotifications: false }), + } as unknown as AgentConfigManager); + const manager = new NotificationManager(mockInboundConnection, { batchNotifications: false }); + const changes: CapabilityChanges = { + hasChanges: true, + toolsChanged: true, + resourcesChanged: false, + promptsChanged: false, + addedServers: [], + removedServers: [], + previous: createMockCapabilities(0, 0, 0), + current: createMockCapabilities(1, 0, 0), + }; + + manager.handleCapabilityChanges(changes); + + expect(mockServer.notification).not.toHaveBeenCalled(); + manager.shutdown(); + }); + it('should send immediate notifications when batching is disabled', () => { const manager = new NotificationManager(mockInboundConnection, { batchNotifications: false }); diff --git a/src/core/types/transport.ts b/src/core/types/transport.ts index bf78b590..5612d940 100644 --- a/src/core/types/transport.ts +++ b/src/core/types/transport.ts @@ -139,7 +139,9 @@ export interface ApplicationConfig { }; readonly asyncLoading?: { readonly enabled?: boolean; + /** @deprecated Compatibility no-op; remove before the next breaking release. */ readonly minServers?: number; + /** @deprecated Compatibility no-op; remove before the next breaking release. */ readonly timeout?: number; readonly batchNotifications?: boolean; readonly batchDelay?: number; @@ -331,10 +333,23 @@ export const applicationConfigSchema = z.object({ asyncLoading: z .object({ enabled: z.boolean().optional().describe('Enable asynchronous MCP server loading'), - minServers: z.number().int().min(0).optional().describe('Minimum servers to wait for before accepting requests'), - timeout: z.number().int().min(0).optional().describe('Initial load timeout in milliseconds'), - batchNotifications: z.boolean().optional().describe('Batch capability change notifications'), - batchDelay: z.number().int().min(0).optional().describe('Batch delay in milliseconds'), + minServers: z + .number() + .int() + .min(0) + .optional() + .describe('Deprecated compatibility no-op; does not gate listener or request readiness'), + timeout: z + .number() + .int() + .min(0) + .optional() + .describe('Deprecated compatibility no-op; does not delay async startup'), + batchNotifications: z + .boolean() + .optional() + .describe('Coalesce capability-change notifications within the batch delay window'), + batchDelay: z.number().int().min(0).optional().describe('Notification coalescing window in milliseconds'), }) .optional(), lazyLoading: z From 6f91db76a30147d4ac10eba46f4ab75dd56d02e7 Mon Sep 17 00:00:00 2001 From: Aditya Datta Date: Fri, 31 Jul 2026 14:09:40 +0530 Subject: [PATCH 3/3] fix(serve): validate async loading settings --- docs/en/guide/advanced/fast-startup.md | 4 ++-- docs/en/reference/health-check.md | 2 +- docs/zh/guide/advanced/fast-startup.md | 4 ++-- docs/zh/reference/health-check.md | 2 +- .../serve/asyncLoadingOptions.test.ts | 7 +++++++ src/commands/serve/asyncLoadingOptions.ts | 7 ++++++- src/commands/serve/index.test.ts | 20 ++++++++----------- 7 files changed, 27 insertions(+), 19 deletions(-) diff --git a/docs/en/guide/advanced/fast-startup.md b/docs/en/guide/advanced/fast-startup.md index 6681f710..064f0ee1 100644 --- a/docs/en/guide/advanced/fast-startup.md +++ b/docs/en/guide/advanced/fast-startup.md @@ -152,7 +152,7 @@ The HTTP listener starts accepting requests immediately. During each loading cyc | 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 ` | `100` | Coalescing window used when notification batching is enabled. | +| `--async-batch-delay ` | `1000` | 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. | @@ -175,7 +175,7 @@ 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. +`/health/ready` reports configuration and supervised-backend readiness, returning `503` while any supervised stdio backend is restarting or in a crash loop. 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` diff --git a/docs/en/reference/health-check.md b/docs/en/reference/health-check.md index b1cfc04f..280dcfcb 100644 --- a/docs/en/reference/health-check.md +++ b/docs/en/reference/health-check.md @@ -170,7 +170,7 @@ X-Uptime-Seconds: 3600 ### `GET /health/ready` -**Description**: Configuration-readiness probe. It does not wait for backend loading or a server-count threshold; use `/health/mcp` to inspect backend loading progress. +**Description**: Configuration and supervised-backend readiness probe. It returns `503` while any supervised stdio backend is restarting or in a crash loop. It does not wait for backend loading or a server-count threshold; use `/health/mcp` to inspect backend loading progress. **Authentication**: None required diff --git a/docs/zh/guide/advanced/fast-startup.md b/docs/zh/guide/advanced/fast-startup.md index 6ab4e84e..7c0255c2 100644 --- a/docs/zh/guide/advanced/fast-startup.md +++ b/docs/zh/guide/advanced/fast-startup.md @@ -146,7 +146,7 @@ HTTP 监听器会立即开始接受请求。在每个加载周期中,1MCP 会 | CLI 选项 | 默认值 | 用途 | | ---------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--async-batch-notifications` | 启用 | 在批处理延迟窗口内合并能力变更事件,减少 `listChanged` 通知。使用 `--no-async-batch-notifications` 可立即发送每个已发布的变更。 | -| `--async-batch-delay <毫秒>` | `100` | 启用通知批处理时的合并窗口。 | +| `--async-batch-delay <毫秒>` | `1000` | 启用通知批处理时的合并窗口。 | | `--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 键时会发出警告,并将在下一主版本中移除。 | @@ -169,7 +169,7 @@ npx -y @1mcp/agent --config mcp.json --enable-async-loading \ 通过简单的 HTTP 端点检查您的 MCP 服务器的状态。 -`/health/ready` 表示配置是否就绪,不会等待达到最少后端连接数。后端加载进度和终态请查看 `/health/mcp`。 +`/health/ready` 表示配置和受监管后端是否就绪;任何受监管的 stdio 后端正在重启或进入崩溃循环时会返回 `503`。它不会等待达到最少后端连接数。后端加载进度和终态请查看 `/health/mcp`。 ### 整体状态:`/health/mcp` diff --git a/docs/zh/reference/health-check.md b/docs/zh/reference/health-check.md index 2253e9cb..e2a811f3 100644 --- a/docs/zh/reference/health-check.md +++ b/docs/zh/reference/health-check.md @@ -155,7 +155,7 @@ X-Uptime-Seconds: 3600 ### `GET /health/ready` -**描述**:配置就绪探针。它不会等待后端加载或达到服务器数量门槛;后端加载进度请查看 `/health/mcp`。 +**描述**:配置和受监管后端就绪探针。任何受监管的 stdio 后端正在重启或进入崩溃循环时,它会返回 `503`。它不会等待后端加载或达到服务器数量门槛;后端加载进度请查看 `/health/mcp`。 **身份验证**:无需 diff --git a/src/commands/serve/asyncLoadingOptions.test.ts b/src/commands/serve/asyncLoadingOptions.test.ts index e6b1e5c9..f26eb02a 100644 --- a/src/commands/serve/asyncLoadingOptions.test.ts +++ b/src/commands/serve/asyncLoadingOptions.test.ts @@ -13,10 +13,17 @@ describe('resolveAsyncLoadingOptions', () => { notifyOnServerReady: true, waitForMinimumServers: 0, initialLoadTimeoutMs: 30000, + batchDelayMs: 1000, }); expect(warn).not.toHaveBeenCalled(); }); + it.each([-1, 1.5, Number.NaN])('rejects an invalid notification batch delay of %s', (batchDelay) => { + expect(() => resolveAsyncLoadingOptions({ 'async-batch-delay': batchDelay }, undefined, vi.fn())).toThrow( + 'Async notification batch delay must be a non-negative integer', + ); + }); + it('treats deprecated CLI or environment thresholds as warning-only no-ops', () => { const warn = vi.fn(); diff --git a/src/commands/serve/asyncLoadingOptions.ts b/src/commands/serve/asyncLoadingOptions.ts index dcc5972b..228766be 100644 --- a/src/commands/serve/asyncLoadingOptions.ts +++ b/src/commands/serve/asyncLoadingOptions.ts @@ -52,6 +52,11 @@ export function resolveAsyncLoadingOptions( ); } + const batchDelayMs = cliOptions['async-batch-delay'] ?? appOptions?.batchDelay ?? 1000; + if (!Number.isInteger(batchDelayMs) || batchDelayMs < 0) { + throw new Error('Async notification batch delay must be a non-negative integer number of milliseconds.'); + } + return { enabled: cliOptions['enable-async-loading'] ?? appOptions?.enabled ?? false, // The canonical spelling wins when both it and the compatibility alias are explicit. @@ -60,6 +65,6 @@ export function resolveAsyncLoadingOptions( waitForMinimumServers: 0, initialLoadTimeoutMs: 30000, batchNotifications: cliOptions['async-batch-notifications'] ?? appOptions?.batchNotifications ?? true, - batchDelayMs: cliOptions['async-batch-delay'] ?? appOptions?.batchDelay ?? 100, + batchDelayMs, }; } diff --git a/src/commands/serve/index.test.ts b/src/commands/serve/index.test.ts index a93fefe1..f8f998d5 100644 --- a/src/commands/serve/index.test.ts +++ b/src/commands/serve/index.test.ts @@ -17,6 +17,7 @@ vi.mock('@src/logger/configureGlobalLogger.js', () => ({ describe('setupServeCommand', () => { afterEach(() => { vi.clearAllMocks(); + vi.unstubAllEnvs(); }); it('does not inject an HTTP transport when no CLI transport flag is passed', async () => { @@ -63,17 +64,12 @@ describe('setupServeCommand', () => { }); 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; - } + vi.stubEnv('ONE_MCP_ASYNC_MIN_SERVERS', '7'); + + await setupServeCommand(yargs([]).env('ONE_MCP').exitProcess(false).help(false).version(false)).parseAsync([ + 'serve', + ]); + + expect(serveCommandMock).toHaveBeenCalledWith(expect.objectContaining({ 'async-min-servers': 7 })); }); });