Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/streaming-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
{
Expand Down
4 changes: 4 additions & 0 deletions src/runtime/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Expand Down Expand Up @@ -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 };
}
Expand Down
1 change: 1 addition & 0 deletions src/tools/bazel-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ describe('Bazel MCP tool definitions', () => {
const properties = tool.inputSchema.properties as Record<string, unknown>;
const streaming = properties.streaming as Record<string, unknown>;
expect(streaming?.type, `${toolName} should expose streaming flag`).toBe('boolean');
expect(streaming?.default, `${toolName} streaming should default to false`).toBe(false);
}
});

Expand Down
9 changes: 5 additions & 4 deletions src/tools/handlers/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -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'],
},
Expand Down Expand Up @@ -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'],
},
Expand All @@ -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.',
Expand Down Expand Up @@ -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,
},
},
},
Expand Down
5 changes: 3 additions & 2 deletions src/tools/handlers/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
{
Expand Down Expand Up @@ -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'],
},
Expand Down Expand Up @@ -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'],
},
Expand Down
7 changes: 4 additions & 3 deletions src/tools/handlers/macos.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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'],
},
Expand All @@ -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'],
},
Expand All @@ -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'],
},
Expand Down
19 changes: 10 additions & 9 deletions src/tools/handlers/multi-platform.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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'],
},
Expand All @@ -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'],
},
Expand All @@ -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'],
},
Expand Down Expand Up @@ -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'],
},
Expand All @@ -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'],
},
Expand All @@ -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'],
},
Expand Down Expand Up @@ -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'],
},
Expand All @@ -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'],
},
Expand All @@ -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'],
},
Expand Down
7 changes: 6 additions & 1 deletion src/tools/handlers/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand Down Expand Up @@ -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.' },
},
Expand Down Expand Up @@ -194,6 +198,7 @@ export async function handle(name: string, args: JsonObject): Promise<ToolCallRe
simulatorId: stringOrUndefined(args.simulatorId),
buildMode: stringOrUndefined(args.buildMode) as BuildMode | undefined,
platform: stringOrUndefined(args.platform) as BuildPlatform | undefined,
streaming: booleanOrUndefined(args.streaming),
});
const lines = Object.entries(updated)
.filter(([, v]) => v !== undefined)
Expand Down
5 changes: 3 additions & 2 deletions src/tools/handlers/spm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand All @@ -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,
},
},
},
Expand All @@ -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,
},
},
},
Expand Down
31 changes: 21 additions & 10 deletions src/tools/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand Down
6 changes: 5 additions & 1 deletion src/tools/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ export const deviceLogCaptures = new Map<string, { child: ReturnType<typeof spaw

export function applyDefaults(args: JsonObject): JsonObject {
const defaults = getDefaults();
if (!defaults || Object.keys(defaults).length === 0) return args;
const merged = { ...args };
if (defaults.target && merged.target === undefined) merged.target = defaults.target;
if (defaults.simulatorName && merged.simulatorName === undefined) merged.simulatorName = defaults.simulatorName;
if (defaults.simulatorId && merged.simulatorId === undefined) merged.simulatorId = defaults.simulatorId;
if (defaults.buildMode && defaults.buildMode !== 'none' && merged.buildMode === undefined) merged.buildMode = defaults.buildMode;
if (defaults.platform && defaults.platform !== 'none' && merged.platform === undefined) merged.platform = defaults.platform;
if (merged.streaming === undefined) merged.streaming = defaults.streaming ?? false;
return merged;
}

Expand All @@ -45,3 +45,7 @@ export function numberOrUndefined(value: unknown): number | undefined {
return typeof value === 'number' ? value : undefined;
}

export function booleanOrUndefined(value: unknown): boolean | undefined {
return typeof value === 'boolean' ? value : undefined;
}

Loading
Loading