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
25 changes: 13 additions & 12 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -475,22 +475,23 @@ Start local development server with hot-reload.
agentcore dev
agentcore dev --agent MyAgent --port 3000
agentcore dev --logs # Non-interactive
agentcore dev --invoke "Hello" --stream # Direct invoke
agentcore dev "Hello" --stream # Invoke running dev server
agentcore dev "Hello" --agent MyAgent # Invoke specific agent

# MCP protocol dev commands
agentcore dev --invoke list-tools
agentcore dev --invoke call-tool --tool myTool --input '{"arg": "value"}'
agentcore dev list-tools
agentcore dev call-tool --tool myTool --input '{"arg": "value"}'
```

| Flag | Description |
| ----------------------- | ------------------------------------------------------ |
| `-p, --port <port>` | Port (default: 8080; MCP uses 8000, A2A uses 9000) |
| `-a, --agent <name>` | Agent to run (required if multiple agents) |
| `-i, --invoke <prompt>` | Invoke running server |
| `-s, --stream` | Stream response (with --invoke) |
| `-l, --logs` | Non-interactive stdout logging |
| `--tool <name>` | MCP tool name (with `--invoke call-tool`) |
| `--input <json>` | MCP tool arguments as JSON (with `--invoke call-tool`) |
| Flag / Argument | Description |
| -------------------- | ---------------------------------------------------- |
| `[prompt]` | Send a prompt to a running dev server |
| `-p, --port <port>` | Port (default: 8080; MCP uses 8000, A2A uses 9000) |
| `-a, --agent <name>` | Agent to run or invoke (required if multiple agents) |
| `-s, --stream` | Stream response when invoking |
| `-l, --logs` | Non-interactive stdout logging |
| `--tool <name>` | MCP tool name (with `call-tool` prompt) |
| `--input <json>` | MCP tool arguments as JSON (with `--tool`) |

### invoke

Expand Down
18 changes: 7 additions & 11 deletions docs/local-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,16 @@ agentcore dev --logs

## Invoking Local Agents

With the dev server running, open another terminal:
Start the dev server in one terminal, then send prompts from another:

```bash
# Interactive chat
agentcore invoke

# Single prompt
agentcore invoke "What can you do?"

# With streaming
agentcore invoke "Tell me a story" --stream
# Terminal 1: start the dev server
agentcore dev --logs

# Direct invoke to running server
agentcore dev --invoke "Hello" --stream
# Terminal 2: send prompts
agentcore dev "What can you do?"
agentcore dev "Tell me a story" --stream
agentcore dev "Hello" --agent MyAgent
```

## Environment Setup
Expand Down
37 changes: 35 additions & 2 deletions src/cli/commands/dev/__tests__/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ describe('dev command', () => {
const result = await runCLI(['dev', '--help'], process.cwd());

expect(result.exitCode).toBe(0);
expect(result.stdout.includes('[prompt]'), 'Should show [prompt] positional argument').toBeTruthy();
expect(result.stdout.includes('--port'), 'Should show --port option').toBeTruthy();
expect(result.stdout.includes('--agent'), 'Should show --agent option').toBeTruthy();
expect(result.stdout.includes('--invoke'), 'Should show --invoke option').toBeTruthy();
expect(result.stdout.includes('--stream'), 'Should show --stream option').toBeTruthy();
expect(result.stdout.includes('--logs'), 'Should show --logs option').toBeTruthy();
expect(result.stdout.includes('8080'), 'Should show default port').toBeTruthy();
});

it('does not show --invoke flag', async () => {
const result = await runCLI(['dev', '--help'], process.cwd());

expect(result.exitCode).toBe(0);
expect(result.stdout.includes('--invoke'), 'Should not show removed --invoke option').toBeFalsy();
});
});

describe('requires project context', () => {
Expand All @@ -28,6 +35,33 @@ describe('dev command', () => {
});
});

describe('positional prompt invoke', () => {
it('exits with helpful error when no server running and no project found', async () => {
// With no dev server running, invoke path shows connection error
const result = await runCLI(['dev', 'Hello agent'], process.cwd());

expect(result.exitCode).toBe(1);
const output = result.stderr.toLowerCase();
expect(
output.includes('dev server not running'),
`Should mention dev server not running, got: ${result.stderr}`
).toBeTruthy();
});

it('does not go through requireProject guard when invoking', async () => {
// Invoke path uses loadProjectConfig (soft check), not requireProject()
// The error should mention the dev server, not the generic project guard message
const result = await runCLI(['dev', 'test prompt'], process.cwd());

expect(result.exitCode).toBe(1);
const output = result.stderr.toLowerCase();
expect(
!output.includes('run agentcore create'),
`Should not show the requireProject guard message, got: ${result.stderr}`
).toBeTruthy();
});
});

describe('flag validation', () => {
it('rejects invalid port number', async () => {
const result = await runCLI(['dev', '--port', 'abc'], process.cwd());
Expand All @@ -46,7 +80,6 @@ describe('dev command', () => {

expect(result.exitCode).toBe(0);
expect(result.stdout.includes('--stream'), 'Should show --stream option').toBeTruthy();
expect(result.stdout.includes('--invoke'), 'Should show --invoke option').toBeTruthy();
});
});
});
52 changes: 31 additions & 21 deletions src/cli/commands/dev/command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ async function invokeDevServer(
console.log(response);
}
} catch (err) {
if (err instanceof Error && err.message.includes('ECONNREFUSED')) {
if (isConnectionRefused(err)) {
console.error(`Error: Dev server not running on port ${port}`);
console.error('Start it with: agentcore dev');
console.error('Start it with: agentcore dev --logs');
} else {
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
}
Expand All @@ -65,16 +65,24 @@ async function invokeA2ADevServer(port: number, prompt: string, headers?: Record
}
process.stdout.write('\n');
} catch (err) {
if (err instanceof Error && err.message.includes('ECONNREFUSED')) {
if (isConnectionRefused(err)) {
console.error(`Error: Dev server not running on port ${port}`);
console.error('Start it with: agentcore dev');
console.error('Start it with: agentcore dev --logs');
} else {
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
}
process.exit(1);
}
}

function isConnectionRefused(err: unknown): boolean {
if (!(err instanceof Error)) return false;
// ConnectionError from invoke.ts wraps fetch failures after retries
if (err.name === 'ConnectionError') return true;
const msg = err.message + (err.cause instanceof Error ? err.cause.message : '');
return msg.includes('ECONNREFUSED') || msg.includes('fetch failed');
}

async function handleMcpInvoke(
port: number,
invokeValue: string,
Expand All @@ -96,8 +104,8 @@ async function handleMcpInvoke(
}
} else if (invokeValue === 'call-tool') {
if (!toolName) {
console.error('Error: --tool is required with --invoke call-tool');
console.error('Usage: agentcore dev --invoke call-tool --tool <name> --input \'{"arg": "value"}\'');
console.error('Error: --tool is required with call-tool');
console.error('Usage: agentcore dev call-tool --tool <name> --input \'{"arg": "value"}\'');
process.exit(1);
}
// Initialize session first, then call tool with the session ID
Expand All @@ -117,14 +125,14 @@ async function handleMcpInvoke(
} else {
console.error(`Error: Unknown MCP invoke command "${invokeValue}"`);
console.error('Usage:');
console.error(' agentcore dev --invoke list-tools');
console.error(' agentcore dev --invoke call-tool --tool <name> --input \'{"arg": "value"}\'');
console.error(' agentcore dev list-tools');
console.error(' agentcore dev call-tool --tool <name> --input \'{"arg": "value"}\'');
process.exit(1);
}
} catch (err) {
if (err instanceof Error && err.message.includes('ECONNREFUSED')) {
if (isConnectionRefused(err)) {
console.error(`Error: Dev server not running on port ${port}`);
console.error('Start it with: agentcore dev');
console.error('Start it with: agentcore dev --logs');
} else {
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
}
Expand All @@ -137,20 +145,20 @@ export const registerDev = (program: Command) => {
.command('dev')
.alias('d')
.description(COMMAND_DESCRIPTIONS.dev)
.argument('[prompt]', 'Send a prompt to a running dev server [non-interactive]')
.option('-p, --port <port>', 'Port for development server', '8080')
.option('-a, --agent <name>', 'Agent to run or invoke (required if multiple agents)')
.option('-i, --invoke <prompt>', 'Invoke running dev server (use --agent if multiple) [non-interactive]')
.option('-s, --stream', 'Stream response when using --invoke [non-interactive]')
.option('-s, --stream', 'Stream response when invoking [non-interactive]')
.option('-l, --logs', 'Run dev server with logs to stdout [non-interactive]')
.option('--tool <name>', 'MCP tool name (used with --invoke call-tool) [non-interactive]')
.option('--input <json>', 'MCP tool arguments as JSON (used with --invoke call-tool) [non-interactive]')
.option('--tool <name>', 'MCP tool name (used with "call-tool" prompt) [non-interactive]')
.option('--input <json>', 'MCP tool arguments as JSON (used with --tool) [non-interactive]')
.option(
'-H, --header <header>',
'Custom header to forward to the agent (format: "Name: Value", repeatable) [non-interactive]',
(val: string, prev: string[]) => [...prev, val],
[] as string[]
)
.action(async opts => {
.action(async (positionalPrompt: string | undefined, opts) => {
try {
const port = parseInt(opts.port, 10);

Expand All @@ -160,9 +168,11 @@ export const registerDev = (program: Command) => {
headers = parseHeaderFlags(opts.header);
}

// If --invoke provided, call the dev server and exit
if (opts.invoke) {
const invokeProject = await loadProjectConfig(getWorkingDirectory());
// If a prompt is provided, invoke a running dev server
const invokePrompt = positionalPrompt;
if (invokePrompt !== undefined) {
const workingDir = getWorkingDirectory();
const invokeProject = await loadProjectConfig(workingDir);

// Determine which agent/port to invoke
let invokePort = port;
Expand Down Expand Up @@ -190,11 +200,11 @@ export const registerDev = (program: Command) => {

// Protocol-aware dispatch
if (protocol === 'MCP') {
await handleMcpInvoke(invokePort, opts.invoke, opts.tool, opts.input, headers);
await handleMcpInvoke(invokePort, invokePrompt, opts.tool, opts.input, headers);
} else if (protocol === 'A2A') {
await invokeA2ADevServer(invokePort, opts.invoke, headers);
await invokeA2ADevServer(invokePort, invokePrompt, headers);
} else {
await invokeDevServer(invokePort, opts.invoke, opts.stream ?? false, headers);
await invokeDevServer(invokePort, invokePrompt, opts.stream ?? false, headers);
}
return;
}
Expand Down
3 changes: 2 additions & 1 deletion src/cli/operations/dev/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,10 @@ export abstract class DevServer {
kill(): void {
if (!this.child || this.child.killed) return;
this.child.kill('SIGTERM');
setTimeout(() => {
const killTimer = setTimeout(() => {
if (this.child && !this.child.killed) this.child.kill('SIGKILL');
}, 2000);
killTimer.unref();
}

/** Mode-specific setup (e.g., venv creation, container image build). Returns false to abort. */
Expand Down
1 change: 1 addition & 0 deletions src/cli/operations/dev/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export {
findAvailablePort,
waitForPort,
waitForServerReady,
createDevServer,
DevServer,
type LogLevel,
Expand Down
2 changes: 1 addition & 1 deletion src/cli/operations/dev/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { DevServer, DevServerOptions } from './dev-server';
* Dev server barrel module.
* Re-exports types, utilities, and the factory function.
*/
export { findAvailablePort, waitForPort } from './utils';
export { findAvailablePort, waitForPort, waitForServerReady } from './utils';
export { DevServer, type LogLevel, type DevServerCallbacks, type DevServerOptions } from './dev-server';
export { CodeZipDevServer } from './codezip-dev-server';
export { ContainerDevServer } from './container-dev-server';
Expand Down
2 changes: 1 addition & 1 deletion src/cli/tui/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const COMMAND_DESCRIPTIONS = {
add: 'Add resources (agent, evaluator, online-eval, memory, credential, target)',
create: 'Create a new AgentCore project',
deploy: 'Deploy project infrastructure to AWS via CDK.',
dev: 'Launch local development server with hot-reload.',
dev: 'Launch local dev server, or invoke an agent locally.',
invoke: 'Invoke a deployed agent endpoint.',
logs: 'Stream or search agent runtime logs.',
package: 'Package agent artifacts without deploying.',
Expand Down
Loading