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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(dev): positional prompt arg for invoking dev server by aidandaly24 · Pull Request #707 · aws/agentcore-cli · GitHub
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(dev): positional prompt arg for invoking dev server by aidandaly24 · Pull Request #707 · aws/agentcore-cli · GitHub
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(dev): positional prompt arg for invoking dev server by aidandaly24 · Pull Request #707 · aws/agentcore-cli · GitHub
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(dev): positional prompt arg for invoking dev server by aidandaly24 · Pull Request #707 · aws/agentcore-cli · GitHub
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(dev): positional prompt arg for invoking dev server by aidandaly24 · Pull Request #707 · aws/agentcore-cli · GitHub
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(dev): positional prompt arg for invoking dev server by aidandaly24 · Pull Request #707 · aws/agentcore-cli · GitHub
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(dev): positional prompt arg for invoking dev server by aidandaly24 · Pull Request #707 · aws/agentcore-cli · GitHub
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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