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
5 changes: 5 additions & 0 deletions .changeset/auto-detect-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@adcp/client": minor
---

Add protocol auto-detection to CLI tool - users can now omit the protocol argument and the CLI will automatically detect whether an endpoint uses MCP or A2A via discovery and URL pattern heuristics
5 changes: 5 additions & 0 deletions .changeset/cli-agent-aliases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@adcp/client": minor
---

Add agent alias support to CLI tool - save agent configurations with short aliases for quick access. Users can now save agents with `--save-auth <alias> <url>` and call them with just `adcp <alias> <tool> <payload>`. Config stored in ~/.adcp/config.json with secure file permissions.
5 changes: 5 additions & 0 deletions .changeset/fix-prepush-hook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@adcp/client": patch
---

Fix pre-push hook to skip slow tests by setting CI=true, matching GitHub Actions behavior and preventing unnecessary test timeouts during git push
77 changes: 66 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,26 +457,81 @@ ORDER BY sequence_number;

## CLI Tool

For development and testing, use the included CLI tool to interact with AdCP agents:
For development and testing, use the included CLI tool to interact with AdCP agents.

### Quick Start with Aliases

Save agents for quick access:

```bash
# List available tools from an agent
npx @adcp/client mcp https://test-agent.adcontextprotocol.org
# Save an agent with an alias
npx @adcp/client --save-auth test https://test-agent.adcontextprotocol.org

# Call a tool with inline JSON payload
npx @adcp/client a2a https://agent.example.com get_products '{"brief":"Coffee brands"}'
# Use the alias
npx @adcp/client test get_products '{"brief":"Coffee brands"}'

# Use a file for payload
npx @adcp/client mcp https://agent.example.com create_media_buy @payload.json
# List saved agents
npx @adcp/client --list-agents
```

### Direct URL Usage

Auto-detect protocol and call directly:

```bash
# Protocol auto-detection (default)
npx @adcp/client https://test-agent.adcontextprotocol.org get_products '{"brief":"Coffee"}'

# Force specific protocol with --protocol flag
npx @adcp/client https://agent.example.com get_products '{"brief":"Coffee"}' --protocol mcp
npx @adcp/client https://agent.example.com list_authorized_properties --protocol a2a

# With authentication
npx @adcp/client mcp https://agent.example.com get_products '{"brief":"..."}' --auth your-token
# List available tools
npx @adcp/client https://agent.example.com

# Use a file for payload
npx @adcp/client https://agent.example.com create_media_buy @payload.json

# JSON output for scripting
npx @adcp/client mcp https://agent.example.com get_products '{"brief":"..."}' --json
npx @adcp/client https://agent.example.com get_products '{"brief":"..."}' --json | jq '.products'
```

**MCP Endpoint Discovery**: The client automatically discovers MCP endpoints. Just provide any URL - it tests your path first, and if no MCP server responds, it tries adding `/mcp`. Works with root domains, custom paths, anything.
### Authentication

Three ways to provide auth tokens (priority order):

```bash
# 1. Explicit flag (highest priority)
npx @adcp/client test get_products '{"brief":"..."}' --auth your-token

# 2. Saved in agent config (recommended)
npx @adcp/client --save-auth prod https://prod-agent.com
# Will prompt for auth token securely

# 3. Environment variable (fallback)
export ADCP_AUTH_TOKEN=your-token
npx @adcp/client test get_products '{"brief":"..."}'
```

### Agent Management

```bash
# Save agent with auth
npx @adcp/client --save-auth prod https://prod-agent.com mcp

# List all saved agents
npx @adcp/client --list-agents

# Remove an agent
npx @adcp/client --remove-agent test

# Show config file location
npx @adcp/client --show-config
```

**Protocol Auto-Detection**: The CLI automatically detects whether an endpoint uses MCP or A2A by checking URL patterns and discovery endpoints. Override with `--protocol mcp` or `--protocol a2a` if needed.

**Config File**: Agent configurations are saved to `~/.adcp/config.json` with secure file permissions (0600).

See [docs/CLI.md](docs/CLI.md) for complete CLI documentation including webhook support for async operations.

Expand Down
230 changes: 230 additions & 0 deletions bin/adcp-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/**
* AdCP CLI Configuration Manager
*
* Manages agent aliases and authentication configuration
*/

const fs = require('fs');
const path = require('path');
const os = require('os');

const CONFIG_DIR = path.join(os.homedir(), '.adcp');
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');

/**
* Get the config file path
*/
function getConfigPath() {
return CONFIG_FILE;
}

/**
* Ensure config directory exists
*/
function ensureConfigDir() {
if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
}
}

/**
* Load configuration from disk
* @returns {Object} Configuration object
*/
function loadConfig() {
try {
if (!fs.existsSync(CONFIG_FILE)) {
return { agents: {}, defaults: {} };
}

const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
return JSON.parse(content);
} catch (error) {
console.error(`Warning: Failed to load config from ${CONFIG_FILE}: ${error.message}`);
return { agents: {}, defaults: {} };
}
}

/**
* Save configuration to disk
* @param {Object} config Configuration object
*/
function saveConfig(config) {
try {
ensureConfigDir();
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
} catch (error) {
throw new Error(`Failed to save config to ${CONFIG_FILE}: ${error.message}`);
}
}

/**
* Get agent configuration by alias
* @param {string} alias Agent alias
* @returns {Object|null} Agent config or null if not found
*/
function getAgent(alias) {
const config = loadConfig();
return config.agents[alias] || null;
}

/**
* List all saved agents
* @returns {Object} Map of alias to agent config
*/
function listAgents() {
const config = loadConfig();
return config.agents || {};
}

/**
* Save an agent configuration
* @param {string} alias Agent alias
* @param {Object} agentConfig Agent configuration
*/
function saveAgent(alias, agentConfig) {
const config = loadConfig();
if (!config.agents) {
config.agents = {};
}

config.agents[alias] = agentConfig;
saveConfig(config);
}

/**
* Remove an agent configuration
* @param {string} alias Agent alias
* @returns {boolean} True if agent was removed
*/
function removeAgent(alias) {
const config = loadConfig();
if (config.agents && config.agents[alias]) {
delete config.agents[alias];
saveConfig(config);
return true;
}
return false;
}

/**
* Check if a string is an agent alias
* @param {string} str String to check
* @returns {boolean} True if string is a saved alias
*/
function isAlias(str) {
const config = loadConfig();
return config.agents && config.agents[str] !== undefined;
}

/**
* Prompt for input securely (for passwords/tokens)
* @param {string} prompt Prompt message
* @param {boolean} hidden Hide input (for passwords)
* @returns {Promise<string>} User input
*/
async function promptSecure(prompt, hidden = true) {
const readline = require('readline');

return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

if (hidden) {
// Disable echo for password input
const stdin = process.stdin;
const originalSetRawMode = stdin.setRawMode;

// Mute output
rl.stdoutMuted = true;
rl._writeToOutput = function _writeToOutput(stringToWrite) {
if (!rl.stdoutMuted) {
rl.output.write(stringToWrite);
}
};
}

rl.question(prompt, (answer) => {
rl.close();
if (hidden) {
console.log(''); // New line after hidden input
}
resolve(answer.trim());
});
});
}

/**
* Interactive agent setup
* @param {string} alias Agent alias
* @param {string} url Agent URL (optional)
* @param {string} protocol Protocol (optional)
* @param {string} authToken Auth token (optional)
* @param {boolean} nonInteractive Skip prompts if all required args provided
*/
async function interactiveSetup(alias, url = null, protocol = null, authToken = null, nonInteractive = false) {
// Non-interactive mode: if URL is provided, just save it
if (nonInteractive && url) {
const agentConfig = { url };
if (protocol) agentConfig.protocol = protocol;
if (authToken) agentConfig.auth_token = authToken;

saveAgent(alias, agentConfig);
console.log(`\n✅ Agent '${alias}' saved to ${CONFIG_FILE}`);
console.log(`\nYou can now use: adcp ${alias} <tool> <payload>`);
return;
}

console.log(`\n📝 Setting up agent: ${alias}\n`);

// Get URL if not provided
if (!url) {
url = await promptSecure('Agent URL: ', false);
}

// Get protocol if not provided (optional, can auto-detect)
if (!protocol) {
const protocolInput = await promptSecure('Protocol (mcp/a2a, or leave blank to auto-detect): ', false);
protocol = protocolInput || null;
}

// Get auth token if not provided
if (!authToken) {
authToken = await promptSecure('Auth token (leave blank if not needed): ', true);
authToken = authToken || null;
}

// Build config
const agentConfig = {
url: url
};

if (protocol) {
agentConfig.protocol = protocol;
}

if (authToken) {
agentConfig.auth_token = authToken;
}

// Save
saveAgent(alias, agentConfig);

console.log(`\n✅ Agent '${alias}' saved to ${CONFIG_FILE}`);
console.log(`\nYou can now use: adcp ${alias} <tool> <payload>`);
}

module.exports = {
getConfigPath,
loadConfig,
saveConfig,
getAgent,
listAgents,
saveAgent,
removeAgent,
isAlias,
promptSecure,
interactiveSetup
};
Loading