diff --git a/docs/streaming-output.md b/docs/streaming-output.md index d1a8365..6ef4a0b 100644 --- a/docs/streaming-output.md +++ b/docs/streaming-output.md @@ -4,7 +4,7 @@ Status: **Implemented** ## Overview -Long-running Bazel commands (build, test, query, clean) can stream stdout/stderr incrementally instead of buffering the entire output until completion. This gives agents and CLI users real-time feedback on build progress. +Long-running Bazel commands (build, test, query, clean) can stream stdout/stderr incrementally instead of buffering the entire output until completion. **Streaming is off by default** (`streaming: false`) to keep MCP tool responses compact. Opt in with `streaming: true` when you want live progress notifications. ## Supported Tools @@ -18,7 +18,7 @@ Long-running Bazel commands (build, test, query, clean) can stream stdout/stderr ## MCP Protocol -Set `streaming: true` in the tool arguments and include a `_meta.progressToken` in the request: +Set `streaming: true` in the tool arguments (or via `bazel_ios_set_defaults`) and include a `_meta.progressToken` in the request: ```json { diff --git a/src/runtime/config.ts b/src/runtime/config.ts index 98467f2..70450ab 100644 --- a/src/runtime/config.ts +++ b/src/runtime/config.ts @@ -81,6 +81,7 @@ export function activateProfile(name: string): SessionDefaults { simulatorId: profile.defaultSimulatorId || config.defaults.simulatorId, buildMode: profile.defaultBuildMode || config.defaults.buildMode, platform: profile.defaultPlatform || config.defaults.platform, + streaming: profile.streaming ?? config.defaults.streaming, }; return { ...config.defaults }; } @@ -141,6 +142,9 @@ function applyFileConfig(fileConfig: FileConfig, filePath: string): void { if (fileConfig.defaultTarget) { config.defaults.target = config.defaults.target || fileConfig.defaultTarget; } + if (fileConfig.defaultStreaming !== undefined && config.defaults.streaming === undefined) { + config.defaults.streaming = fileConfig.defaultStreaming; + } if (fileConfig.profiles) { config.profiles = { ...config.profiles, ...fileConfig.profiles }; } diff --git a/src/tools/bazel-tools.test.ts b/src/tools/bazel-tools.test.ts index 316c271..858d025 100644 --- a/src/tools/bazel-tools.test.ts +++ b/src/tools/bazel-tools.test.ts @@ -330,6 +330,7 @@ describe('Bazel MCP tool definitions', () => { const properties = tool.inputSchema.properties as Record; const streaming = properties.streaming as Record; expect(streaming?.type, `${toolName} should expose streaming flag`).toBe('boolean'); + expect(streaming?.default, `${toolName} streaming should default to false`).toBe(false); } }); diff --git a/src/tools/handlers/build.ts b/src/tools/handlers/build.ts index c7eee10..8f386c6 100644 --- a/src/tools/handlers/build.ts +++ b/src/tools/handlers/build.ts @@ -32,6 +32,7 @@ import type { } from '../../types/index.js'; import { formatCommandResult, structuredCommandResult, toolResult, toolText } from '../../utils/output.js'; import { numberOrUndefined, prependWarning, resolveSimulatorFromArgs, stringOrUndefined } from '../helpers.js'; +import { STREAMING_PROPERTY } from '../schema-constants.js'; interface LcovFile { name: string; total: number; covered: number } @@ -92,7 +93,7 @@ export const definitions: ToolDefinition[] = [ startupArgs: { type: 'array', items: { type: 'string' } }, extraArgs: { type: 'array', items: { type: 'string' } }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream build output incrementally via progress notifications.' }, + streaming: STREAMING_PROPERTY, }, required: ['target'], }, @@ -126,7 +127,7 @@ export const definitions: ToolDefinition[] = [ description: 'Environment variables injected into the launched app.', }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream build output incrementally via progress notifications.' }, + streaming: STREAMING_PROPERTY, }, required: ['target'], }, @@ -145,7 +146,7 @@ export const definitions: ToolDefinition[] = [ startupArgs: { type: 'array', items: { type: 'string' } }, extraArgs: { type: 'array', items: { type: 'string' } }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream test output incrementally via progress notifications.' }, + streaming: STREAMING_PROPERTY, minimizeSimulator: { type: 'boolean', description: 'Minimize Simulator.app windows while the test runs.', @@ -222,7 +223,7 @@ export const definitions: ToolDefinition[] = [ properties: { expunge: { type: 'boolean', description: 'Remove entire output base (bazel clean --expunge).' }, startupArgs: { type: 'array', items: { type: 'string' } }, - streaming: { type: 'boolean', description: 'Stream output incrementally via progress notifications.' }, + streaming: STREAMING_PROPERTY, }, }, }, diff --git a/src/tools/handlers/device.ts b/src/tools/handlers/device.ts index 6f86bed..2a19235 100644 --- a/src/tools/handlers/device.ts +++ b/src/tools/handlers/device.ts @@ -24,6 +24,7 @@ import { findAppBundle, readBundleId } from '../../core/simulators.js'; import { getConfig } from '../../runtime/config.js'; import { formatCommandResult, structuredCommandResult, toolResult, toolText } from '../../utils/output.js'; import { deviceLogCaptures, stringOrUndefined, numberOrUndefined } from '../helpers.js'; +import { STREAMING_PROPERTY } from '../schema-constants.js'; export const definitions: ToolDefinition[] = [ { @@ -64,7 +65,7 @@ export const definitions: ToolDefinition[] = [ description: 'Environment variables injected into the launched app on device.', }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream build output incrementally via progress notifications.' }, + streaming: STREAMING_PROPERTY, }, required: ['target'], }, @@ -133,7 +134,7 @@ export const definitions: ToolDefinition[] = [ startupArgs: { type: 'array', items: { type: 'string' } }, extraArgs: { type: 'array', items: { type: 'string' } }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream test output incrementally.' }, + streaming: STREAMING_PROPERTY, }, required: ['target'], }, diff --git a/src/tools/handlers/macos.ts b/src/tools/handlers/macos.ts index f4aefe3..981b6d2 100644 --- a/src/tools/handlers/macos.ts +++ b/src/tools/handlers/macos.ts @@ -1,5 +1,6 @@ import type { JsonObject, ToolCallResult, ToolDefinition, BuildArgs, TestArgs, TargetKind, BuildMode } from '../../types/index.js'; import { stringOrUndefined, numberOrUndefined } from '../helpers.js'; +import { STREAMING_PROPERTY } from '../schema-constants.js'; import { buildCommandArgs, configArgs, discoverExpression, modeArgs, requireLabel, runBazel, asStringArray, testFilterArgs } from '../../core/bazel.js'; import { findAppBundle, readBundleId } from '../../core/simulators.js'; import { formatCommandResult, structuredCommandResult, toolResult, toolText } from '../../utils/output.js'; @@ -19,7 +20,7 @@ export const definitions: ToolDefinition[] = [ startupArgs: { type: 'array', items: { type: 'string' } }, extraArgs: { type: 'array', items: { type: 'string' } }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream build output incrementally via progress notifications.' }, + streaming: STREAMING_PROPERTY, }, required: ['target'], }, @@ -37,7 +38,7 @@ export const definitions: ToolDefinition[] = [ extraArgs: { type: 'array', items: { type: 'string' } }, runArgs: { type: 'array', items: { type: 'string' }, description: 'Arguments passed to the launched binary after --.' }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream output incrementally via progress notifications.' }, + streaming: STREAMING_PROPERTY, }, required: ['target'], }, @@ -54,7 +55,7 @@ export const definitions: ToolDefinition[] = [ startupArgs: { type: 'array', items: { type: 'string' } }, extraArgs: { type: 'array', items: { type: 'string' } }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream test output incrementally via progress notifications.' }, + streaming: STREAMING_PROPERTY, }, required: ['target'], }, diff --git a/src/tools/handlers/multi-platform.ts b/src/tools/handlers/multi-platform.ts index 119e950..aab9807 100644 --- a/src/tools/handlers/multi-platform.ts +++ b/src/tools/handlers/multi-platform.ts @@ -1,5 +1,6 @@ import type { JsonObject, ToolCallResult, ToolDefinition, BuildArgs, TestArgs, BuildPlatform, BuildMode, TargetKind } from '../../types/index.js'; import { stringOrUndefined, numberOrUndefined } from '../helpers.js'; +import { STREAMING_PROPERTY } from '../schema-constants.js'; import { buildCommandArgs, configArgs, discoverExpression, modeArgs, platformArgs, requireLabel, runBazel, asStringArray, testFilterArgs } from '../../core/bazel.js'; import { formatCommandResult, structuredCommandResult, toolResult, toolText } from '../../utils/output.js'; @@ -16,7 +17,7 @@ export const definitions: ToolDefinition[] = [ startupArgs: { type: 'array', items: { type: 'string' } }, extraArgs: { type: 'array', items: { type: 'string' } }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream build output incrementally.' }, + streaming: STREAMING_PROPERTY, }, required: ['target'], }, @@ -33,7 +34,7 @@ export const definitions: ToolDefinition[] = [ startupArgs: { type: 'array', items: { type: 'string' } }, extraArgs: { type: 'array', items: { type: 'string' } }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream test output incrementally.' }, + streaming: STREAMING_PROPERTY, }, required: ['target'], }, @@ -51,7 +52,7 @@ export const definitions: ToolDefinition[] = [ extraArgs: { type: 'array', items: { type: 'string' } }, runArgs: { type: 'array', items: { type: 'string' }, description: 'Arguments passed after --.' }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream output incrementally.' }, + streaming: STREAMING_PROPERTY, }, required: ['target'], }, @@ -81,7 +82,7 @@ export const definitions: ToolDefinition[] = [ startupArgs: { type: 'array', items: { type: 'string' } }, extraArgs: { type: 'array', items: { type: 'string' } }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream build output incrementally.' }, + streaming: STREAMING_PROPERTY, }, required: ['target'], }, @@ -98,7 +99,7 @@ export const definitions: ToolDefinition[] = [ startupArgs: { type: 'array', items: { type: 'string' } }, extraArgs: { type: 'array', items: { type: 'string' } }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream test output incrementally.' }, + streaming: STREAMING_PROPERTY, }, required: ['target'], }, @@ -116,7 +117,7 @@ export const definitions: ToolDefinition[] = [ extraArgs: { type: 'array', items: { type: 'string' } }, runArgs: { type: 'array', items: { type: 'string' }, description: 'Arguments passed after --.' }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream output incrementally.' }, + streaming: STREAMING_PROPERTY, }, required: ['target'], }, @@ -146,7 +147,7 @@ export const definitions: ToolDefinition[] = [ startupArgs: { type: 'array', items: { type: 'string' } }, extraArgs: { type: 'array', items: { type: 'string' } }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream build output incrementally.' }, + streaming: STREAMING_PROPERTY, }, required: ['target'], }, @@ -163,7 +164,7 @@ export const definitions: ToolDefinition[] = [ startupArgs: { type: 'array', items: { type: 'string' } }, extraArgs: { type: 'array', items: { type: 'string' } }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream test output incrementally.' }, + streaming: STREAMING_PROPERTY, }, required: ['target'], }, @@ -181,7 +182,7 @@ export const definitions: ToolDefinition[] = [ extraArgs: { type: 'array', items: { type: 'string' } }, runArgs: { type: 'array', items: { type: 'string' }, description: 'Arguments passed after --.' }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream output incrementally.' }, + streaming: STREAMING_PROPERTY, }, required: ['target'], }, diff --git a/src/tools/handlers/session.ts b/src/tools/handlers/session.ts index 7c7cc8b..b153ca5 100644 --- a/src/tools/handlers/session.ts +++ b/src/tools/handlers/session.ts @@ -18,7 +18,7 @@ import { import type { BuildMode, BuildPlatform, JsonObject, ToolCallResult, ToolDefinition } from '../../types/index.js'; import { formatCommandResult, toolText } from '../../utils/output.js'; import { runCommand } from '../../utils/process.js'; -import { stringOrUndefined } from '../helpers.js'; +import { stringOrUndefined, booleanOrUndefined } from '../helpers.js'; import { bazelToolDefinitions } from '../bazel-tools.js'; export const definitions: ToolDefinition[] = [ @@ -50,6 +50,10 @@ export const definitions: ToolDefinition[] = [ simulatorId: { type: 'string', description: 'Default simulator UDID.' }, buildMode: { type: 'string', enum: ['none', 'debug', 'release', 'release_with_symbols'] }, platform: { type: 'string', enum: ['none', 'simulator', 'device'] }, + streaming: { + type: 'boolean', + description: 'Default streaming for build/test tools. Defaults to false. Set true for live progress.', + }, profile: { type: 'string', description: 'Activate a named profile from config file.' }, clear: { type: 'boolean', description: 'Clear all session defaults.' }, }, @@ -194,6 +198,7 @@ export async function handle(name: string, args: JsonObject): Promise v !== undefined) diff --git a/src/tools/handlers/spm.ts b/src/tools/handlers/spm.ts index 039215a..c3224fa 100644 --- a/src/tools/handlers/spm.ts +++ b/src/tools/handlers/spm.ts @@ -5,6 +5,7 @@ import { runCommand } from '../../utils/process.js'; import { formatCommandResult, structuredCommandResult, toolResult, toolText } from '../../utils/output.js'; import { getConfig } from '../../runtime/config.js'; import { stringOrUndefined, numberOrUndefined } from '../helpers.js'; +import { STREAMING_PROPERTY } from '../schema-constants.js'; import { asStringArray } from '../../core/bazel.js'; export const definitions: ToolDefinition[] = [ @@ -19,7 +20,7 @@ export const definitions: ToolDefinition[] = [ target: { type: 'string', description: 'Specific target to build (default: all targets).' }, extraArgs: { type: 'array', items: { type: 'string' }, description: 'Additional arguments passed to swift build.' }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream build output incrementally via progress notifications.' }, + streaming: STREAMING_PROPERTY, }, }, }, @@ -34,7 +35,7 @@ export const definitions: ToolDefinition[] = [ configuration: { type: 'string', enum: ['debug', 'release'], description: 'Build configuration (default: debug).' }, extraArgs: { type: 'array', items: { type: 'string' }, description: 'Additional arguments passed to swift test.' }, timeoutSeconds: { type: 'number' }, - streaming: { type: 'boolean', description: 'Stream test output incrementally via progress notifications.' }, + streaming: STREAMING_PROPERTY, }, }, }, diff --git a/src/tools/helpers.test.ts b/src/tools/helpers.test.ts index a60708e..e8c9fd5 100644 --- a/src/tools/helpers.test.ts +++ b/src/tools/helpers.test.ts @@ -46,50 +46,50 @@ describe('prependWarning', () => { describe('applyDefaults', () => { afterEach(() => clearDefaults()); - it('returns args unchanged when no defaults set', () => { + it('returns args with streaming=false when no defaults set', () => { clearDefaults(); const args = { target: '//app' }; - expect(applyDefaults(args)).toEqual({ target: '//app' }); + expect(applyDefaults(args)).toEqual({ target: '//app', streaming: false }); }); it('merges target default when args.target is undefined', () => { setDefaults({ target: '//default' }); - expect(applyDefaults({})).toEqual({ target: '//default' }); + expect(applyDefaults({})).toEqual({ target: '//default', streaming: false }); }); it('does NOT override explicitly provided target', () => { setDefaults({ target: '//default' }); - expect(applyDefaults({ target: '//explicit' })).toEqual({ target: '//explicit' }); + expect(applyDefaults({ target: '//explicit' })).toEqual({ target: '//explicit', streaming: false }); }); it('merges simulatorName', () => { setDefaults({ simulatorName: 'iPhone 15' }); - expect(applyDefaults({})).toEqual({ simulatorName: 'iPhone 15' }); + expect(applyDefaults({})).toEqual({ simulatorName: 'iPhone 15', streaming: false }); }); it('merges simulatorId', () => { setDefaults({ simulatorId: 'ABC-123' }); - expect(applyDefaults({})).toEqual({ simulatorId: 'ABC-123' }); + expect(applyDefaults({})).toEqual({ simulatorId: 'ABC-123', streaming: false }); }); it('merges buildMode', () => { setDefaults({ buildMode: 'debug' }); - expect(applyDefaults({})).toEqual({ buildMode: 'debug' }); + expect(applyDefaults({})).toEqual({ buildMode: 'debug', streaming: false }); }); it('merges platform', () => { setDefaults({ platform: 'simulator' }); - expect(applyDefaults({})).toEqual({ platform: 'simulator' }); + expect(applyDefaults({})).toEqual({ platform: 'simulator', streaming: false }); }); it('does NOT merge buildMode=none', () => { setDefaults({ buildMode: 'none' }); - expect(applyDefaults({})).toEqual({}); + expect(applyDefaults({})).toEqual({ streaming: false }); }); it('does NOT merge platform=none', () => { setDefaults({ platform: 'none' }); - expect(applyDefaults({})).toEqual({}); + expect(applyDefaults({})).toEqual({ streaming: false }); }); it('handles multiple defaults at once', () => { @@ -99,8 +99,19 @@ describe('applyDefaults', () => { simulatorName: 'iPhone 15', buildMode: 'debug', platform: 'simulator', + streaming: false, }); }); + + it('merges streaming default from session', () => { + setDefaults({ streaming: true }); + expect(applyDefaults({})).toEqual({ streaming: true }); + }); + + it('does NOT override explicitly provided streaming', () => { + setDefaults({ streaming: true }); + expect(applyDefaults({ streaming: false })).toEqual({ streaming: false }); + }); }); describe('nextLogCaptureId', () => { diff --git a/src/tools/helpers.ts b/src/tools/helpers.ts index 4c16e86..1d08977 100644 --- a/src/tools/helpers.ts +++ b/src/tools/helpers.ts @@ -15,13 +15,13 @@ export const deviceLogCaptures = new Map; enabledWorkflows?: string[]; }