diff --git a/docs/commands.md b/docs/commands.md index eeaab1930..0777362e4 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -823,19 +823,19 @@ agentcore dev list-tools agentcore dev call-tool --tool myTool --input '{"arg": "value"}' ``` -| Flag / Argument | Description | -| ---------------------- | --------------------------------------------------------------------- | -| `[prompt]` | Send a prompt to a running dev server | -| `-p, --port ` | Port (default: 8080; MCP uses 8000, A2A uses 9000) | -| `-r, --runtime ` | Runtime to run or invoke (required if multiple runtimes) | -| `-s, --stream` | Stream response when invoking | -| `-l, --logs` | Non-interactive stdout logging | -| `--tool ` | MCP tool name (with `call-tool` prompt) | -| `--input ` | MCP tool arguments as JSON (with `--tool`) | -| `-H, --header ` | Custom header (`"Name: Value"`, repeatable) | -| `--exec` | Execute a shell command in the running dev container (Container only) | -| `-b, --no-browser` | Use terminal TUI instead of web-based chat UI | -| `--no-traces` | Disable local OTEL trace collection | +| Flag / Argument | Description | +| ---------------------- | ----------------------------------------------------------------------------------------- | +| `[prompt]` | Send a prompt to a running dev server | +| `-p, --port ` | Port (default: 8080; MCP uses 8000; A2A starts at 9000 and offsets for multiple runtimes) | +| `-r, --runtime ` | Runtime to run or invoke (required if multiple runtimes) | +| `-s, --stream` | Stream response when invoking | +| `-l, --logs` | Non-interactive stdout logging | +| `--tool ` | MCP tool name (with `call-tool` prompt) | +| `--input ` | MCP tool arguments as JSON (with `--tool`) | +| `-H, --header ` | Custom header (`"Name: Value"`, repeatable) | +| `--exec` | Execute a shell command in the running dev container (Container only) | +| `-b, --no-browser` | Use terminal TUI instead of web-based chat UI | +| `--no-traces` | Disable local OTEL trace collection | ### invoke diff --git a/docs/container-builds.md b/docs/container-builds.md index d785adac3..004937214 100644 --- a/docs/container-builds.md +++ b/docs/container-builds.md @@ -59,6 +59,9 @@ For TypeScript agents, the generated `Dockerfile` uses `public.ecr.aws/docker/li - **Entrypoint**: `npx tsx main.ts` — no compile step, so dev and container runtime share the same entry shape - **Ports**: Exposes 8080 / 8000 / 9000 to match the HTTP / MCP / A2A contract +During `agentcore dev`, each container receives a unique host port. Multiple A2A agents therefore map ports such as +`9000:9000` and `9001:9000` without conflicting on the host. + Example `agentcore.json` for a TypeScript container agent: ```json diff --git a/src/cli/commands/dev/command.tsx b/src/cli/commands/dev/command.tsx index 41176398b..7913cbb77 100644 --- a/src/cli/commands/dev/command.tsx +++ b/src/cli/commands/dev/command.tsx @@ -16,8 +16,8 @@ import { callMcpTool, createDevServer, findAvailablePort, - getAgentPort, getDevConfig, + getDevPort, getDevSupportedAgents, getEndpointUrl, invokeAgent, @@ -265,7 +265,6 @@ export const registerDev = (program: Command) => { let invokePort = port; let targetAgent = invokeProject?.runtimes[0]; if (opts.runtime && invokeProject) { - invokePort = getAgentPort(invokeProject, opts.runtime, port, portExplicit); targetAgent = invokeProject.runtimes.find(a => a.name === opts.runtime); } else if (invokeProject && invokeProject.runtimes.length > 1 && !opts.runtime) { const names = invokeProject.runtimes.map(a => a.name).join(', '); @@ -275,13 +274,13 @@ export const registerDev = (program: Command) => { } const protocol = targetAgent?.protocol ?? 'HTTP'; + if (targetAgent && invokeProject) { + invokePort = getDevPort(invokeProject, targetAgent.name, protocol, port, portExplicit); + } recorder.set({ agent_protocol: standardize(AgentProtocol, protocol.toLowerCase()), }); - if (protocol === 'A2A') invokePort = 9000; - else if (protocol === 'MCP') invokePort = 8000; - if (protocol === 'MCP') { await handleMcpInvoke(invokePort, invokePrompt, opts.tool, opts.input, headers); } else if (protocol === 'A2A') { @@ -405,31 +404,23 @@ export const registerDev = (program: Command) => { agent_protocol: standardize(AgentProtocol, config.protocol.toLowerCase()), }); - const isA2A = config.protocol === 'A2A'; const isMcp = config.protocol === 'MCP'; - const isHttp = !isA2A && !isMcp; - const fixedPort = isA2A - ? 9000 - : isMcp - ? 8000 - : getAgentPort(project, config.agentName, port, portExplicit); - if (isHttp && !portExplicit && fixedPort !== port) { + const targetPort = getDevPort(project, config.agentName, config.protocol, port, portExplicit); + if (!isMcp && !portExplicit && targetPort !== port) { const idx = project.runtimes.findIndex(a => a.name === config.agentName); console.log( - `Runtime "${config.agentName}" is at index ${idx}; using port ${fixedPort} (pass --port ${fixedPort} to override).` + `Runtime "${config.agentName}" is at index ${idx}; using port ${targetPort} (pass --port ${targetPort} to override).` ); } - const actualPort = await findAvailablePort(fixedPort); - if ((isA2A || isMcp) && actualPort !== fixedPort) { - throw new ValidationError( - `Port ${fixedPort} is in use. ${config.protocol} agents require port ${fixedPort}.` - ); + const actualPort = await findAvailablePort(targetPort); + if (isMcp && actualPort !== targetPort) { + throw new ValidationError(`Port ${targetPort} is in use. MCP agents require port ${targetPort}.`); } // An explicit -p must be honored literally; if it's taken, fail fast instead of // silently rebinding to a different port (the silent-shift behavior #1079 removes). - if (isHttp && portExplicit && actualPort !== fixedPort) { + if (!isMcp && portExplicit && actualPort !== targetPort) { throw new ValidationError( - `Port ${fixedPort} is in use. Free it or pass a different --port (no port is chosen automatically when --port is set explicitly).` + `Port ${targetPort} is in use. Free it or pass a different --port (no port is chosen automatically when --port is set explicitly).` ); } @@ -440,8 +431,8 @@ export const registerDev = (program: Command) => { const logger = new ExecLogger({ command: 'dev' }); - if (actualPort !== fixedPort) { - console.log(`Port ${fixedPort} in use, using ${actualPort}`); + if (actualPort !== targetPort) { + console.log(`Port ${targetPort} in use, using ${actualPort}`); } console.log(`Starting dev server...`); diff --git a/src/cli/operations/dev/__tests__/codezip-dev-server.test.ts b/src/cli/operations/dev/__tests__/codezip-dev-server.test.ts index d68f48cf9..f0e0a2fe1 100644 --- a/src/cli/operations/dev/__tests__/codezip-dev-server.test.ts +++ b/src/cli/operations/dev/__tests__/codezip-dev-server.test.ts @@ -38,6 +38,7 @@ const defaultOptions: DevServerOptions = { port: 8080, envVars: { MY_KEY: 'secre describe('CodeZipDevServer spawn config', () => { beforeEach(() => { + mockSpawn.mockClear(); mockSpawn.mockReturnValue(createMockChildProcess()); }); @@ -106,7 +107,7 @@ describe('CodeZipDevServer spawn config', () => { ); }); - it('non-HTTP: passes env vars including PORT and LOCAL_DEV', async () => { + it('A2A: passes the selected port and agent-card URL in the environment', async () => { const config: DevConfig = { agentName: 'A2aAgent', module: 'main.py', @@ -123,6 +124,7 @@ describe('CodeZipDevServer spawn config', () => { const spawnCall = mockSpawn.mock.calls[0]!; const env = spawnCall[2].env; expect(env.PORT).toBe('8080'); + expect(env.AGENTCORE_RUNTIME_URL).toBe('http://localhost:8080/'); expect(env.LOCAL_DEV).toBe('1'); expect(env.MY_KEY).toBe('secret'); }); diff --git a/src/cli/operations/dev/__tests__/config.test.ts b/src/cli/operations/dev/__tests__/config.test.ts index 38b096451..253623c4c 100644 --- a/src/cli/operations/dev/__tests__/config.test.ts +++ b/src/cli/operations/dev/__tests__/config.test.ts @@ -1,5 +1,5 @@ import type { AgentCoreProjectSpec, DirectoryPath, FilePath } from '../../../../schema'; -import { getAgentPort, getDevConfig, getDevSupportedAgents } from '../config'; +import { getAgentPort, getDevConfig, getDevPort, getDevSupportedAgents } from '../config'; import { describe, expect, it } from 'vitest'; // Helper to cast strings to branded path types for testing @@ -671,6 +671,57 @@ describe('getAgentPort', () => { }); }); +describe('getDevPort', () => { + const project: AgentCoreProjectSpec = { + name: 'TestProject', + version: 1, + managedBy: 'CDK' as const, + runtimes: [ + { + name: 'AgentA', + build: 'CodeZip', + runtimeVersion: 'PYTHON_3_12', + entrypoint: filePath('main.py'), + codeLocation: dirPath('./agents/a'), + protocol: 'A2A', + }, + { + name: 'AgentB', + build: 'CodeZip', + runtimeVersion: 'PYTHON_3_12', + entrypoint: filePath('main.py'), + codeLocation: dirPath('./agents/b'), + protocol: 'A2A', + }, + ], + memories: [], + knowledgeBases: [], + credentials: [], + evaluators: [], + onlineEvalConfigs: [], + agentCoreGateways: [], + policyEngines: [], + configBundles: [], + abTests: [], + harnesses: [], + datasets: [], + payments: [], + }; + + it('offsets the A2A default port by runtime index', () => { + expect(getDevPort(project, 'AgentA', 'A2A', 8080)).toBe(9000); + expect(getDevPort(project, 'AgentB', 'A2A', 8080)).toBe(9001); + }); + + it('honors an explicit port for A2A', () => { + expect(getDevPort(project, 'AgentB', 'A2A', 8788, true)).toBe(8788); + }); + + it('keeps MCP on its fixed framework port', () => { + expect(getDevPort(project, 'AgentB', 'MCP', 8788, true)).toBe(8000); + }); +}); + describe('getDevSupportedAgents', () => { it('returns empty array when project is null', () => { expect(getDevSupportedAgents(null)).toEqual([]); diff --git a/src/cli/operations/dev/__tests__/container-dev-server.test.ts b/src/cli/operations/dev/__tests__/container-dev-server.test.ts index aebf552b3..babb19b98 100644 --- a/src/cli/operations/dev/__tests__/container-dev-server.test.ts +++ b/src/cli/operations/dev/__tests__/container-dev-server.test.ts @@ -343,6 +343,33 @@ describe('ContainerDevServer', () => { expect(spawnArgs).toContain(`9000:${CONTAINER_INTERNAL_PORT}`); }); + it('maps a unique A2A host port to the A2A container port', async () => { + mockSuccessfulPrepare(); + const config = { ...defaultConfig, protocol: 'A2A' as const }; + const options = { ...defaultOptions, port: 9001 }; + + const server = new ContainerDevServer(config, options); + await server.start(); + + const spawnArgs = getSpawnArgs(); + expect(spawnArgs).toContain('9001:9000'); + expect(spawnArgs).toContain('PORT=9000'); + expect(spawnArgs).toContain('AGENTCORE_RUNTIME_URL=http://localhost:9001/'); + }); + + it('maps an MCP host port to the MCP container port', async () => { + mockSuccessfulPrepare(); + const config = { ...defaultConfig, protocol: 'MCP' as const }; + const options = { ...defaultOptions, port: 8000 }; + + const server = new ContainerDevServer(config, options); + await server.start(); + + const spawnArgs = getSpawnArgs(); + expect(spawnArgs).toContain('8000:8000'); + expect(spawnArgs).toContain('PORT=8000'); + }); + it('includes user-provided environment variables', async () => { mockSuccessfulPrepare(); diff --git a/src/cli/operations/dev/codezip-dev-server.ts b/src/cli/operations/dev/codezip-dev-server.ts index 024d251fb..3a6f9f98a 100644 --- a/src/cli/operations/dev/codezip-dev-server.ts +++ b/src/cli/operations/dev/codezip-dev-server.ts @@ -145,6 +145,9 @@ export class CodeZipDevServer extends DevServer { if (protocol === 'MCP') { env.FASTMCP_PORT = String(port); } + if (protocol === 'A2A') { + env.AGENTCORE_RUNTIME_URL = `http://localhost:${port}/`; + } if (!isPython) { // TS entrypoint is already a file path like "main.ts" — pass it straight to tsx. diff --git a/src/cli/operations/dev/config.ts b/src/cli/operations/dev/config.ts index 0d5a358e7..dd83b5c36 100644 --- a/src/cli/operations/dev/config.ts +++ b/src/cli/operations/dev/config.ts @@ -1,5 +1,6 @@ import { ConfigIO, findConfigRoot } from '../../../lib'; import type { AgentCoreProjectSpec, AgentEnvSpec, BuildType, ProtocolMode } from '../../../schema'; +import { A2A_DEFAULT_PORT, MCP_DEFAULT_PORT } from './constants'; import { dirname, isAbsolute, join } from 'node:path'; export interface DevConfig { @@ -90,6 +91,27 @@ export function getAgentPort( return index >= 0 ? basePort + index : basePort; } +/** + * Resolve the local development port for an agent. + * + * A2A starts at its framework default and offsets by runtime index so multiple + * agents can run together. MCP remains fixed because FastMCP currently ignores + * the port environment variable. Explicit ports are honored for all other + * protocols. + */ +export function getDevPort( + project: AgentCoreProjectSpec | null, + agentName: string, + protocol: ProtocolMode, + basePort: number, + explicit = false +): number { + if (protocol === 'MCP') return MCP_DEFAULT_PORT; + if (explicit) return basePort; + const protocolBasePort = protocol === 'A2A' ? A2A_DEFAULT_PORT : basePort; + return getAgentPort(project, agentName, protocolBasePort); +} + /** * Derives dev server configuration from project config. * Falls back to sensible defaults if no config is available. diff --git a/src/cli/operations/dev/constants.ts b/src/cli/operations/dev/constants.ts new file mode 100644 index 000000000..1519da92a --- /dev/null +++ b/src/cli/operations/dev/constants.ts @@ -0,0 +1,2 @@ +export const MCP_DEFAULT_PORT = 8000; +export const A2A_DEFAULT_PORT = 9000; diff --git a/src/cli/operations/dev/container-dev-server.ts b/src/cli/operations/dev/container-dev-server.ts index f203c07f4..d53c7e372 100644 --- a/src/cli/operations/dev/container-dev-server.ts +++ b/src/cli/operations/dev/container-dev-server.ts @@ -2,6 +2,7 @@ import { CONTAINER_INTERNAL_PORT, DOCKERFILE_NAME, getDockerfilePath } from '../ import { getCustomBuildArgs, getUvBuildArgs } from '../../../lib/packaging/build-args'; import { ensureBuildContextDockerignore } from '../../../lib/packaging/build-context-dockerignore'; import { detectContainerRuntime } from '../../external-requirements/detect'; +import { A2A_DEFAULT_PORT, MCP_DEFAULT_PORT } from './constants'; import { DevServer, type LogLevel, type SpawnConfig } from './dev-server'; import { waitForServerReady } from './utils'; import { type ChildProcess, spawn, spawnSync } from 'child_process'; @@ -177,6 +178,12 @@ export class ContainerDevServer extends DevServer { protected getSpawnConfig(): SpawnConfig { const { port, envVars = {} } = this.options; + const internalPort = + this.config.protocol === 'A2A' + ? A2A_DEFAULT_PORT + : this.config.protocol === 'MCP' + ? MCP_DEFAULT_PORT + : CONTAINER_INTERNAL_PORT; // Forward AWS credentials from host environment into the container. // When explicit credentials are present, omit AWS_PROFILE so SDK credential @@ -242,7 +249,8 @@ export class ContainerDevServer extends DevServer { ...awsConfigEnv, ...containerEnvVars, LOCAL_DEV: '1', - PORT: String(CONTAINER_INTERNAL_PORT), + PORT: String(internalPort), + ...(this.config.protocol === 'A2A' ? { AGENTCORE_RUNTIME_URL: `http://localhost:${port}/` } : {}), }).flatMap(([k, v]) => ['-e', `${k}=${v}`]); return { @@ -254,7 +262,7 @@ export class ContainerDevServer extends DevServer { this.containerName, ...awsMountArgs, '-p', - `${port}:${CONTAINER_INTERNAL_PORT}`, + `${port}:${internalPort}`, ...envArgs, this.imageName, ], diff --git a/src/cli/operations/dev/index.ts b/src/cli/operations/dev/index.ts index 4a7b73ed6..90e400954 100644 --- a/src/cli/operations/dev/index.ts +++ b/src/cli/operations/dev/index.ts @@ -9,7 +9,14 @@ export { type DevServerOptions, } from './server'; -export { getDevConfig, getDevSupportedAgents, getAgentPort, loadProjectConfig, type DevConfig } from './config'; +export { + getDevConfig, + getDevSupportedAgents, + getAgentPort, + getDevPort, + loadProjectConfig, + type DevConfig, +} from './config'; export { invokeAgent, invokeAgentStreaming, invokeForProtocol } from './invoke'; diff --git a/src/cli/operations/dev/web-ui/__tests__/start-port.test.ts b/src/cli/operations/dev/web-ui/__tests__/start-port.test.ts index 34e1e6cc1..63b55af58 100644 --- a/src/cli/operations/dev/web-ui/__tests__/start-port.test.ts +++ b/src/cli/operations/dev/web-ui/__tests__/start-port.test.ts @@ -13,17 +13,15 @@ describe('resolveAgentTargetPort', () => { expect(resolveAgentTargetPort({ ...base, protocol: 'HTTP', agentName: 'missing', agentIndex: -1 })).toBe(7778); }); - it('uses framework-fixed ports for A2A and MCP regardless of -p', () => { + it('offsets A2A by runtime index and keeps MCP fixed', () => { expect( resolveAgentTargetPort({ ...base, protocol: 'A2A', agentName: 'A', agentIndex: 3, - agentBasePort: 8788, - selectedAgent: 'A', }) - ).toBe(9000); + ).toBe(9003); expect( resolveAgentTargetPort({ ...base, @@ -36,11 +34,11 @@ describe('resolveAgentTargetPort', () => { ).toBe(8000); }); - it('honors an explicit -p literally for the selected runtime (no offset)', () => { + it.each(['HTTP', 'A2A'])('honors an explicit -p literally for a selected %s runtime', protocol => { expect( resolveAgentTargetPort({ ...base, - protocol: 'HTTP', + protocol, agentName: 'AgentB', agentIndex: 1, agentBasePort: 8788, diff --git a/src/cli/operations/dev/web-ui/handlers/start.ts b/src/cli/operations/dev/web-ui/handlers/start.ts index 64afcabf5..967d40163 100644 --- a/src/cli/operations/dev/web-ui/handlers/start.ts +++ b/src/cli/operations/dev/web-ui/handlers/start.ts @@ -1,3 +1,4 @@ +import { A2A_DEFAULT_PORT, MCP_DEFAULT_PORT } from '../../constants'; import { type DevServerCallbacks, createDevServer, findAvailablePort } from '../../server'; import { waitForServerReady } from '../../utils'; import type { RouteContext } from './route-context'; @@ -73,7 +74,8 @@ export async function handleStart( /** * Resolve the target port a web-UI-served agent should bind to. * - * - A2A/MCP agents use their framework-fixed ports (9000 / 8000). + * - A2A agents use 9000 + runtime index so multiple agents can run together. + * - MCP agents use their framework-fixed port (8000). * - When `-p`/`--port` was set explicitly (`agentBasePort !== undefined`), the * *selected* runtime is honored literally (binds exactly `agentBasePort`, no * offset). All other runtimes fall back to the default `uiPort + 1 + index` @@ -90,12 +92,12 @@ export function resolveAgentTargetPort(args: { selectedAgent?: string; }): number { const { protocol, agentName, agentIndex, uiPort, agentBasePort, selectedAgent } = args; - if (protocol === 'A2A') return 9000; - if (protocol === 'MCP') return 8000; const safeIndex = agentIndex >= 0 ? agentIndex : 0; + if (protocol === 'MCP') return MCP_DEFAULT_PORT; if (agentBasePort !== undefined && agentName === selectedAgent) { return agentBasePort; } + if (protocol === 'A2A') return A2A_DEFAULT_PORT + safeIndex; return uiPort + 1 + safeIndex; } @@ -120,14 +122,11 @@ async function doStartAgent( const agentIndex = ctx.options.agents.findIndex(a => a.name === agentName); const { onLog } = ctx.options; - // Several frameworks bind to a fixed port that ignores the PORT env var: - // - A2A: serve_a2a() accepts port as a function parameter, not from env → 9000 - // - MCP (FastMCP): pydantic BaseSettings init kwarg overrides env → 8000 + // FastMCP binds to a fixed port that ignores the port environment variable. // TS HTTP agents read PORT env var so we can assign any available port. // For Python HTTP agents, uvicorn takes --port as a CLI arg so we can assign any port. const isA2A = config.protocol === 'A2A'; const isMCP = config.protocol === 'MCP'; - const fixedPort = isA2A ? 9000 : isMCP ? 8000 : undefined; const isTsHttp = !config.isPython && config.protocol === 'HTTP'; const targetPort = resolveAgentTargetPort({ protocol: config.protocol, @@ -141,13 +140,12 @@ async function doStartAgent( // fail fast instead of silently rebinding (the silent-shift behavior #1079 removes). const portIsExplicit = ctx.options.agentBasePort !== undefined && agentName === ctx.options.selectedAgent; const agentPort = await findAvailablePort(targetPort); - if (fixedPort && agentPort !== fixedPort) { - const reason = isA2A ? 'A2A agents require port 9000.' : 'MCP agents require port 8000 (FastMCP default).'; + if (isMCP && agentPort !== targetPort) { return { success: false, name: agentName, port: 0, - error: `Port ${fixedPort} is in use. ${reason}`, + error: `Port ${targetPort} is in use. MCP agents require port ${targetPort} (FastMCP default).`, }; } if (portIsExplicit && agentPort !== targetPort) { @@ -195,6 +193,7 @@ async function doStartAgent( const agentEnvVars = { ...baseEnvVars, OTEL_SERVICE_NAME: agentName, + ...(isA2A ? { AGENTCORE_RUNTIME_URL: `http://localhost:${agentPort}/` } : {}), ...(isTsHttp ? { PORT: String(agentPort) } : {}), }; diff --git a/src/cli/tui/hooks/useDevServer.ts b/src/cli/tui/hooks/useDevServer.ts index 6f3cd3722..540574c62 100644 --- a/src/cli/tui/hooks/useDevServer.ts +++ b/src/cli/tui/hooks/useDevServer.ts @@ -13,8 +13,8 @@ import { createDevServer, fetchA2AAgentCard, findAvailablePort, - getAgentPort, getDevConfig, + getDevPort, getEndpointUrl, invokeA2AStreaming, invokeAgentStreaming, @@ -153,13 +153,8 @@ export function useDevServer(options: { }); setLogFilePath(loggerRef.current.getRelativeLogPath()); - // A2A servers always use port 9000, MCP servers use port 8000 (framework defaults, not configurable via env) - const isA2A = config.protocol === 'A2A'; const isMcp = config.protocol === 'MCP'; - // HTTP: honor an explicit -p literally; otherwise offset by the runtime index - // so parallel runtimes bind distinct ports (consistent with the --logs path). - const httpPort = getAgentPort(project, config.agentName, targetPort, options.portExplicit); - const fixedPort = isA2A ? 9000 : isMcp ? 8000 : httpPort; + const targetAgentPort = getDevPort(project, config.agentName, config.protocol, targetPort, options.portExplicit); // On restart, reuse the same port. On initial start, find an available port. // If restart times out waiting for port, fall back to finding a new one. @@ -173,29 +168,29 @@ export function useDevServer(options: { } let port: number; - if (isA2A || isMcp) { - // A2A/MCP must use their fixed ports; check availability but don't auto-assign another - const available = await findAvailablePort(fixedPort); - if (available !== fixedPort) { - addLog('error', `Port ${fixedPort} is in use. ${config.protocol} agents require port ${fixedPort}.`); + if (isMcp) { + // FastMCP currently ignores port environment variables, so it must use its framework default. + const available = await findAvailablePort(targetAgentPort); + if (available !== targetAgentPort) { + addLog('error', `Port ${targetAgentPort} is in use. MCP agents require port ${targetAgentPort}.`); setStatus('error'); return; } - port = fixedPort; + port = targetAgentPort; } else { - port = isRestart && portFree ? actualPortRef.current : await findAvailablePort(fixedPort); - if (!isRestart && port !== fixedPort) { + port = isRestart && portFree ? actualPortRef.current : await findAvailablePort(targetAgentPort); + if (!isRestart && port !== targetAgentPort) { // An explicit -p must be honored literally; if it's taken, surface an error // instead of silently rebinding (the silent-shift behavior #1079 removes). if (options.portExplicit) { addLog( 'error', - `Port ${fixedPort} is in use. Free it or pass a different --port (no port is chosen automatically when --port is set explicitly).` + `Port ${targetAgentPort} is in use. Free it or pass a different --port (no port is chosen automatically when --port is set explicitly).` ); setStatus('error'); return; } - addLog('warn', `Port ${fixedPort} in use, using ${port}`); + addLog('warn', `Port ${targetAgentPort} in use, using ${port}`); } } actualPortRef.current = port;