diff --git a/docs/en/guide/advanced/fast-startup.md b/docs/en/guide/advanced/fast-startup.md index 94c8e00a..064f0ee1 100644 --- a/docs/en/guide/advanced/fast-startup.md +++ b/docs/en/guide/advanced/fast-startup.md @@ -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 ` | `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. | + +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 @@ -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 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` 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..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**: Readiness probe to determine if the service is ready to accept requests. +**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 a1ee2fe0..7c0255c2 100644 --- a/docs/zh/guide/advanced/fast-startup.md +++ b/docs/zh/guide/advanced/fast-startup.md @@ -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 <毫秒>` | `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 键时会发出警告,并将在下一主版本中移除。 | + +较长的批处理延迟可以减少通知突发,但会推迟能力快照发布后的通知;它不会延迟监听器可用性,也不会创建就绪门槛。 + +```bash +npx -y @1mcp/agent --config mcp.json --enable-async-loading \ + --async-notify-on-snapshot \ + --async-batch-delay 250 +``` ## 配置 @@ -150,6 +169,8 @@ sequenceDiagram 通过简单的 HTTP 端点检查您的 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 79f2a916..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` -**描述**:就绪性探针,用于确定服务是否已准备好接受请求。 +**描述**:配置和受监管后端就绪探针。任何受监管的 stdio 后端正在重启或进入崩溃循环时,它会返回 `503`。它不会等待后端加载或达到服务器数量门槛;后端加载进度请查看 `/health/mcp`。 **身份验证**:无需 diff --git a/src/commands/serve/asyncLoadingOptions.test.ts b/src/commands/serve/asyncLoadingOptions.test.ts new file mode 100644 index 00000000..f26eb02a --- /dev/null +++ b/src/commands/serve/asyncLoadingOptions.test.ts @@ -0,0 +1,74 @@ +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, + 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(); + + 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..228766be --- /dev/null +++ b/src/commands/serve/asyncLoadingOptions.ts @@ -0,0 +1,70 @@ +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.', + ); + } + + 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. + 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, + }; +} diff --git a/src/commands/serve/index.test.ts b/src/commands/serve/index.test.ts index 2b6e4562..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 () => { @@ -52,4 +53,23 @@ 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 () => { + 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 })); + }); }); 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