Skip to content

Commit 814a1d0

Browse files
committed
feat(dev): auto-start dev server on invoke
When running `agentcore dev "Hello"`, the CLI now automatically starts a dev server if none is running, invokes it, and shuts it down after. If a server is already running (via --logs or TUI), it reuses it. - Export waitForServerReady from dev operations barrel - Probe target port before invoking; auto-start if nothing listening - Use Promise.race to bail early if server crashes during startup - Silent startup (no "Starting dev server..." output) - Helpful error when outside project with no server running - Update tests, docs, and command descriptions Constraint: Must not add latency when server is already running Rejected: Always require separate terminal for dev server | poor UX parity with agentcore invoke Confidence: high Scope-risk: narrow
1 parent daf5bb2 commit 814a1d0

7 files changed

Lines changed: 101 additions & 29 deletions

File tree

docs/commands.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ Start local development server with hot-reload.
475475
agentcore dev
476476
agentcore dev --agent MyAgent --port 3000
477477
agentcore dev --logs # Non-interactive
478-
agentcore dev "Hello" --stream # Invoke running server
478+
agentcore dev "Hello" --stream # Invoke agent (auto-starts server)
479479
agentcore dev "Hello" --agent MyAgent # Invoke specific agent
480480

481481
# MCP protocol dev commands
@@ -485,7 +485,7 @@ agentcore dev call-tool --tool myTool --input '{"arg": "value"}'
485485

486486
| Flag / Argument | Description |
487487
| -------------------- | ---------------------------------------------------- |
488-
| `[prompt]` | Invoke running dev server with this prompt |
488+
| `[prompt]` | Invoke local agent (auto-starts server if needed) |
489489
| `-p, --port <port>` | Port (default: 8080; MCP uses 8000, A2A uses 9000) |
490490
| `-a, --agent <name>` | Agent to run or invoke (required if multiple agents) |
491491
| `-s, --stream` | Stream response when invoking |

docs/local-development.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ agentcore dev --logs
2020

2121
## Invoking Local Agents
2222

23-
With the dev server running, open another terminal:
23+
Send a prompt to your agent — the dev server starts automatically if it isn't already running:
2424

2525
```bash
26-
# Single prompt
26+
# Single prompt (auto-starts server, invokes, shuts down)
2727
agentcore dev "What can you do?"
2828

2929
# With streaming
@@ -33,6 +33,9 @@ agentcore dev "Tell me a story" --stream
3333
agentcore dev "Hello" --agent MyAgent
3434
```
3535

36+
If you already have `agentcore dev` or `agentcore dev --logs` running in another terminal, the invoke reuses that
37+
server.
38+
3639
## Environment Setup
3740

3841
### Python Virtual Environment

src/cli/commands/dev/__tests__/dev.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,29 +36,29 @@ describe('dev command', () => {
3636
});
3737

3838
describe('positional prompt invoke', () => {
39-
it('attempts invoke when positional prompt is provided', async () => {
40-
// With no dev server running, the invoke path triggers a connection error
39+
it('exits with helpful error when no server running and no project found', async () => {
40+
// With no dev server running and no project, the auto-start path
41+
// detects both conditions and shows a helpful error
4142
const result = await runCLI(['dev', 'Hello agent'], process.cwd());
4243

4344
expect(result.exitCode).toBe(1);
44-
// Should attempt to connect to dev server and fail — not show a project error
4545
const output = result.stderr.toLowerCase();
4646
expect(
47-
output.includes('fetch failed') || output.includes('econnrefused') || output.includes('dev server not running'),
48-
`Should attempt invoke and fail with connection error, got: ${result.stderr}`
47+
output.includes('no dev server running'),
48+
`Should mention no dev server running, got: ${result.stderr}`
4949
).toBeTruthy();
5050
});
5151

52-
it('does not require project context when invoking', async () => {
53-
// Invoke path loads config but does not call requireProject()
54-
// So the error should be about connection, not missing project
52+
it('does not go through requireProject guard when invoking', async () => {
53+
// Invoke path uses loadProjectConfig (soft check), not requireProject()
54+
// The error should mention the dev server, not the generic project guard message
5555
const result = await runCLI(['dev', 'test prompt'], process.cwd());
5656

5757
expect(result.exitCode).toBe(1);
5858
const output = result.stderr.toLowerCase();
5959
expect(
60-
!output.includes('no agentcore project found'),
61-
`Should not fail with project error when prompt is provided, got: ${result.stderr}`
60+
!output.includes('run agentcore create'),
61+
`Should not show the requireProject guard message, got: ${result.stderr}`
6262
).toBeTruthy();
6363
});
6464
});

src/cli/commands/dev/command.tsx

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
invokeForProtocol,
1515
listMcpTools,
1616
loadProjectConfig,
17+
waitForServerReady,
1718
} from '../../operations/dev';
1819
import { getGatewayEnvVars } from '../../operations/dev/gateway-env.js';
1920
import { FatalError } from '../../tui/components';
@@ -137,7 +138,7 @@ export const registerDev = (program: Command) => {
137138
.command('dev')
138139
.alias('d')
139140
.description(COMMAND_DESCRIPTIONS.dev)
140-
.argument('[prompt]', 'Invoke running dev server with this prompt [non-interactive]')
141+
.argument('[prompt]', 'Invoke local agent with this prompt (auto-starts server if needed) [non-interactive]')
141142
.option('-p, --port <port>', 'Port for development server', '8080')
142143
.option('-a, --agent <name>', 'Agent to run or invoke (required if multiple agents)')
143144
.option('-s, --stream', 'Stream response when invoking [non-interactive]')
@@ -160,10 +161,11 @@ export const registerDev = (program: Command) => {
160161
headers = parseHeaderFlags(opts.header);
161162
}
162163

163-
// If a prompt is provided, call the dev server and exit
164+
// If a prompt is provided, invoke the dev server (auto-starting if needed)
164165
const invokePrompt = positionalPrompt;
165166
if (invokePrompt) {
166-
const invokeProject = await loadProjectConfig(getWorkingDirectory());
167+
const workingDir = getWorkingDirectory();
168+
const invokeProject = await loadProjectConfig(workingDir);
167169

168170
// Determine which agent/port to invoke
169171
let invokePort = port;
@@ -184,18 +186,84 @@ export const registerDev = (program: Command) => {
184186
if (protocol === 'A2A') invokePort = 9000;
185187
else if (protocol === 'MCP') invokePort = 8000;
186188

187-
// Show model info if available (not applicable to MCP)
188-
if (protocol !== 'MCP' && targetAgent?.modelProvider) {
189-
console.log(`Provider: ${targetAgent.modelProvider}`);
189+
// Check if a dev server is already running on the target port
190+
const serverRunning = await waitForServerReady(invokePort, 500);
191+
192+
// Auto-start a dev server if none is running
193+
let autoStartedServer: ReturnType<typeof createDevServer> | undefined;
194+
if (!serverRunning) {
195+
if (!invokeProject) {
196+
console.error('Error: No dev server running and no agentcore project found.');
197+
console.error('Start a dev server first: agentcore dev');
198+
process.exit(1);
199+
}
200+
201+
const configRoot = findConfigRoot(workingDir);
202+
const envVars = configRoot ? await readEnvFile(configRoot) : {};
203+
const gatewayEnvVars = await getGatewayEnvVars();
204+
const mergedEnvVars = { ...gatewayEnvVars, ...envVars };
205+
const agentName = opts.agent ?? invokeProject.agents[0]?.name;
206+
const config = getDevConfig(workingDir, invokeProject, configRoot ?? undefined, agentName);
207+
208+
if (!config) {
209+
console.error('Error: No dev-supported agents found.');
210+
process.exit(1);
211+
}
212+
213+
const serverErrors: string[] = [];
214+
let resolveServerExit: () => void;
215+
const serverExitPromise = new Promise<void>(resolve => {
216+
resolveServerExit = resolve;
217+
});
218+
219+
const devCallbacks = {
220+
onLog: (level: string, msg: string) => {
221+
if (level === 'error') serverErrors.push(msg);
222+
},
223+
onExit: () => {
224+
resolveServerExit();
225+
},
226+
};
227+
228+
const server = createDevServer(config, {
229+
port: invokePort,
230+
envVars: mergedEnvVars,
231+
callbacks: devCallbacks,
232+
});
233+
await server.start();
234+
235+
// Wait for server to accept connections, bail early if process crashes
236+
const ready = await Promise.race([waitForServerReady(invokePort), serverExitPromise.then(() => false)]);
237+
238+
if (!ready) {
239+
if (serverErrors.length > 0) {
240+
console.error(serverErrors.slice(-5).join('\n'));
241+
}
242+
console.error('Error: Dev server failed to start. Run "agentcore dev --logs" for details.');
243+
server.kill();
244+
process.exit(1);
245+
}
246+
autoStartedServer = server;
190247
}
191248

192-
// Protocol-aware dispatch
193-
if (protocol === 'MCP') {
194-
await handleMcpInvoke(invokePort, invokePrompt, opts.tool, opts.input, headers);
195-
} else if (protocol === 'A2A') {
196-
await invokeA2ADevServer(invokePort, invokePrompt, headers);
197-
} else {
198-
await invokeDevServer(invokePort, invokePrompt, opts.stream ?? false, headers);
249+
try {
250+
// Show model info if available (not applicable to MCP)
251+
if (protocol !== 'MCP' && targetAgent?.modelProvider) {
252+
console.log(`Provider: ${targetAgent.modelProvider}`);
253+
}
254+
255+
// Protocol-aware dispatch
256+
if (protocol === 'MCP') {
257+
await handleMcpInvoke(invokePort, invokePrompt, opts.tool, opts.input, headers);
258+
} else if (protocol === 'A2A') {
259+
await invokeA2ADevServer(invokePort, invokePrompt, headers);
260+
} else {
261+
await invokeDevServer(invokePort, invokePrompt, opts.stream ?? false, headers);
262+
}
263+
} finally {
264+
if (autoStartedServer) {
265+
autoStartedServer.kill();
266+
}
199267
}
200268
return;
201269
}

src/cli/operations/dev/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export {
22
findAvailablePort,
33
waitForPort,
4+
waitForServerReady,
45
createDevServer,
56
DevServer,
67
type LogLevel,

src/cli/operations/dev/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { DevServer, DevServerOptions } from './dev-server';
77
* Dev server barrel module.
88
* Re-exports types, utilities, and the factory function.
99
*/
10-
export { findAvailablePort, waitForPort } from './utils';
10+
export { findAvailablePort, waitForPort, waitForServerReady } from './utils';
1111
export { DevServer, type LogLevel, type DevServerCallbacks, type DevServerOptions } from './dev-server';
1212
export { CodeZipDevServer } from './codezip-dev-server';
1313
export { ContainerDevServer } from './container-dev-server';

src/cli/tui/copy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const COMMAND_DESCRIPTIONS = {
3232
add: 'Add resources (agent, evaluator, online-eval, memory, credential, target)',
3333
create: 'Create a new AgentCore project',
3434
deploy: 'Deploy project infrastructure to AWS via CDK.',
35-
dev: 'Launch local dev server, or invoke a running one.',
35+
dev: 'Launch local dev server, or invoke an agent locally.',
3636
invoke: 'Invoke a deployed agent endpoint.',
3737
logs: 'Stream or search agent runtime logs.',
3838
package: 'Package agent artifacts without deploying.',

0 commit comments

Comments
 (0)