diff --git a/.changeset/auto-detect-protocol.md b/.changeset/auto-detect-protocol.md new file mode 100644 index 000000000..1707f7603 --- /dev/null +++ b/.changeset/auto-detect-protocol.md @@ -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 diff --git a/.changeset/cli-agent-aliases.md b/.changeset/cli-agent-aliases.md new file mode 100644 index 000000000..ee06437b9 --- /dev/null +++ b/.changeset/cli-agent-aliases.md @@ -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 ` and call them with just `adcp `. Config stored in ~/.adcp/config.json with secure file permissions. diff --git a/.changeset/fix-prepush-hook.md b/.changeset/fix-prepush-hook.md new file mode 100644 index 000000000..749dd5e0b --- /dev/null +++ b/.changeset/fix-prepush-hook.md @@ -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 diff --git a/README.md b/README.md index 826fd0533..63c0b17e0 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/bin/adcp-config.js b/bin/adcp-config.js new file mode 100644 index 000000000..86b25b6c0 --- /dev/null +++ b/bin/adcp-config.js @@ -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} 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} `); + 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} `); +} + +module.exports = { + getConfigPath, + loadConfig, + saveConfig, + getAgent, + listAgents, + saveAgent, + removeAgent, + isAlias, + promptSecure, + interactiveSetup +}; diff --git a/bin/adcp.js b/bin/adcp.js index f65111d9b..0133fd3f1 100755 --- a/bin/adcp.js +++ b/bin/adcp.js @@ -14,9 +14,59 @@ * adcp mcp https://agent.example.com/mcp create_media_buy @payload.json --auth $AGENT_TOKEN */ -const { ADCPClient } = require('../dist/lib/index.js'); +const { ADCPClient, detectProtocol } = require('../dist/lib/index.js'); const { readFileSync } = require('fs'); const { AsyncWebhookHandler } = require('./adcp-async-handler.js'); +const { + getAgent, + listAgents, + isAlias, + interactiveSetup, + removeAgent, + getConfigPath +} = require('./adcp-config.js'); + +/** + * Extract human-readable protocol message from conversation + */ +function extractProtocolMessage(conversation, protocol) { + if (!conversation || conversation.length === 0) return null; + + // Find the last agent response (don't mutate original array) + const agentResponse = [...conversation].reverse().find(msg => msg.role === 'agent'); + if (!agentResponse || !agentResponse.content) return null; + + if (protocol === 'mcp') { + // MCP: The content[].text contains the tool response (JSON stringified) + // This IS the protocol message in MCP + if (agentResponse.content.content && Array.isArray(agentResponse.content.content)) { + const textContent = agentResponse.content.content.find(c => c.type === 'text'); + return textContent?.text || null; + } + if (agentResponse.content.text) { + return agentResponse.content.text; + } + } else if (protocol === 'a2a') { + // A2A: Extract human-readable message from task result + // The message is nested in result.artifacts[0].parts[0].data.message + const result = agentResponse.content.result; + if (result && result.artifacts && result.artifacts.length > 0) { + const artifact = result.artifacts[0]; + if (artifact.parts && artifact.parts.length > 0) { + const data = artifact.parts[0].data; + if (data && data.message) { + return data.message; + } + } + } + // Fallback: check top-level status message + if (result && result.status && result.status.message) { + return result.status.message; + } + } + + return null; +} /** * Display agent info - just calls library method @@ -59,53 +109,70 @@ function printUsage() { AdCP CLI Tool - Direct Agent Communication USAGE: - adcp [tool-name] [payload] [options] + adcp [tool-name] [payload] [options] ARGUMENTS: - protocol Protocol to use: 'mcp' or 'a2a' - agent-url Full URL to the agent endpoint - tool-name Name of the tool to call (optional - omit to list available tools) - payload JSON payload for the tool (default: {}) - - Can be inline JSON: '{"brief":"text"}' - - Can be file path: @payload.json - - Can be stdin: - + agent-alias|url Saved agent alias (e.g., 'test') or full URL to agent endpoint + tool-name Name of the tool to call (optional - omit to list available tools) + payload JSON payload for the tool (default: {}) + - Can be inline JSON: '{"brief":"text"}' + - Can be file path: @payload.json + - Can be stdin: - OPTIONS: - --auth TOKEN Authentication token for the agent - --wait Wait for async/webhook responses (requires ngrok or --local) - --local Use local webhook without ngrok (for local agents only) - --timeout MS Webhook timeout in milliseconds (default: 300000 = 5min) - --help, -h Show this help message - --json Output raw JSON response (default: pretty print) - --debug Show debug information + --protocol PROTO Force protocol: 'mcp' or 'a2a' (default: auto-detect) + --auth TOKEN Authentication token for the agent + --wait Wait for async/webhook responses (requires ngrok or --local) + --local Use local webhook without ngrok (for local agents only) + --timeout MS Webhook timeout in milliseconds (default: 300000 = 5min) + --help, -h Show this help message + --json Output raw JSON response (default: pretty print) + --debug Show debug information + +AGENT MANAGEMENT: + --save-auth [url] Save agent configuration interactively + --list-agents List all saved agents + --remove-agent Remove saved agent configuration + --show-config Show config file location EXAMPLES: - # List available tools - adcp mcp https://agent.example.com/mcp - adcp a2a https://creative.adcontextprotocol.org + # Save an agent for easy access + adcp --save-auth test https://test-agent.adcontextprotocol.org - # Simple product discovery - adcp mcp https://agent.example.com/mcp get_products '{"brief":"coffee brands"}' + # Use saved agent alias (auto-detect protocol) + adcp test + adcp test get_products '{"brief":"travel"}' - # With authentication - adcp a2a https://agent.example.com list_creative_formats '{}' --auth your_token + # List saved agents + adcp --list-agents - # Wait for async response (requires ngrok) - adcp mcp https://agent.example.com/mcp create_media_buy @payload.json --auth $TOKEN --wait + # Auto-detect protocol with URL + adcp https://test-agent.adcontextprotocol.org get_products '{"brief":"coffee"}' - # Wait for async response from local agent (no ngrok needed) - adcp mcp http://localhost:3000/mcp create_media_buy @payload.json --wait --local + # Force specific protocol + adcp https://agent.example.com get_products '{"brief":"coffee"}' --protocol mcp + adcp test list_authorized_properties --protocol a2a - # From file - adcp mcp https://agent.example.com/mcp create_media_buy @payload.json --auth $TOKEN + # Override saved auth token + adcp test get_products '{"brief":"..."}' --auth different-token - # From stdin - echo '{"brief":"travel"}' | adcp mcp https://agent.example.com/mcp get_products - + # Wait for async response (requires ngrok) + adcp test create_media_buy @payload.json --wait + + # From file or stdin + adcp test create_media_buy @payload.json + echo '{"brief":"travel"}' | adcp test get_products - + + # JSON output for scripting + adcp test get_products '{"brief":"travel"}' --json | jq '.products[0]' ENVIRONMENT VARIABLES: ADCP_AUTH_TOKEN Default authentication token (overridden by --auth) ADCP_DEBUG Enable debug mode (set to 'true') +CONFIG FILE: + Agents are saved to ~/.adcp/config.json + EXIT CODES: 0 Success 1 General error @@ -123,8 +190,75 @@ async function main() { process.exit(0); } + // Handle agent management commands + if (args[0] === '--save-auth') { + const alias = args[1]; + const url = args[2] || null; + const protocol = args[3] || null; + + if (!alias) { + console.error('ERROR: --save-auth requires an alias\n'); + console.error('Usage: adcp --save-auth [url] [protocol]\n'); + process.exit(2); + } + + // Non-interactive if URL is provided + const nonInteractive = !!url; + await interactiveSetup(alias, url, protocol, null, nonInteractive); + process.exit(0); + } + + if (args[0] === '--list-agents') { + const agents = listAgents(); + const aliases = Object.keys(agents); + + if (aliases.length === 0) { + console.log('\nNo saved agents found.'); + console.log('Use: adcp --save-auth \n'); + process.exit(0); + } + + console.log('\nšŸ“‹ Saved Agents:\n'); + aliases.forEach(alias => { + const agent = agents[alias]; + console.log(` ${alias}`); + console.log(` URL: ${agent.url}`); + if (agent.protocol) { + console.log(` Protocol: ${agent.protocol}`); + } + if (agent.auth_token) { + console.log(` Auth: configured`); + } + console.log(''); + }); + console.log(`Config: ${getConfigPath()}\n`); + process.exit(0); + } + + if (args[0] === '--remove-agent') { + const alias = args[1]; + + if (!alias) { + console.error('ERROR: --remove-agent requires an alias\n'); + process.exit(2); + } + + if (removeAgent(alias)) { + console.log(`\nāœ… Removed agent '${alias}'\n`); + } else { + console.error(`\nERROR: Agent '${alias}' not found\n`); + process.exit(2); + } + process.exit(0); + } + + if (args[0] === '--show-config') { + console.log(`\nConfig file: ${getConfigPath()}\n`); + process.exit(0); + } + // Parse arguments - if (args.length < 2) { + if (args.length < 1) { console.error('ERROR: Missing required arguments\n'); printUsage(); process.exit(2); @@ -133,6 +267,8 @@ async function main() { // Parse options first const authIndex = args.indexOf('--auth'); const authToken = authIndex !== -1 ? args[authIndex + 1] : process.env.ADCP_AUTH_TOKEN; + const protocolIndex = args.indexOf('--protocol'); + const protocolFlag = protocolIndex !== -1 ? args[protocolIndex + 1] : null; const jsonOutput = args.includes('--json'); const debug = args.includes('--debug') || process.env.ADCP_DEBUG === 'true'; const waitForAsync = args.includes('--wait'); @@ -140,21 +276,66 @@ async function main() { const timeoutIndex = args.indexOf('--timeout'); const timeout = timeoutIndex !== -1 ? parseInt(args[timeoutIndex + 1]) : 300000; + // Validate protocol flag if provided + if (protocolFlag && protocolFlag !== 'mcp' && protocolFlag !== 'a2a') { + console.error(`ERROR: Invalid protocol '${protocolFlag}'. Must be 'mcp' or 'a2a'\n`); + printUsage(); + process.exit(2); + } + // Filter out flag arguments to find positional arguments const positionalArgs = args.filter(arg => !arg.startsWith('--') && arg !== authToken && // Don't include the auth token value + arg !== protocolFlag && // Don't include the protocol value arg !== (timeoutIndex !== -1 ? args[timeoutIndex + 1] : null) // Don't include timeout value ); - const protocol = positionalArgs[0]; - const agentUrl = positionalArgs[1]; - const toolName = positionalArgs[2]; // Optional - if not provided, list tools - let payloadArg = positionalArgs[3] || '{}'; + // Determine if first arg is alias or URL + let protocol = protocolFlag; // Start with flag if provided + let agentUrl; + let toolName; + let payloadArg; + let savedAgent = null; + + const firstArg = positionalArgs[0]; + + // Check if first arg is a saved alias + if (isAlias(firstArg)) { + // Alias mode - load saved agent config + savedAgent = getAgent(firstArg); + agentUrl = savedAgent.url; + + // Protocol priority: --protocol flag > saved config > auto-detect + if (!protocol) { + protocol = savedAgent.protocol || null; + } + + toolName = positionalArgs[1]; + payloadArg = positionalArgs[2] || '{}'; + + // Use saved auth token if not overridden + if (!authToken && savedAgent.auth_token) { + authToken = savedAgent.auth_token; + } - // Validate protocol - if (protocol !== 'mcp' && protocol !== 'a2a') { - console.error(`ERROR: Invalid protocol '${protocol}'. Must be 'mcp' or 'a2a'\n`); + if (debug) { + console.error(`DEBUG: Using saved agent '${firstArg}'`); + console.error(` URL: ${agentUrl}`); + if (protocol) { + console.error(` Protocol: ${protocol}`); + } + console.error(''); + } + } else if (firstArg && (firstArg.startsWith('http://') || firstArg.startsWith('https://'))) { + // URL mode + agentUrl = firstArg; + toolName = positionalArgs[1]; + payloadArg = positionalArgs[2] || '{}'; + // protocol already set from flag, or null for auto-detect + } else { + console.error(`ERROR: First argument must be an alias or URL\n`); + console.error(`Available aliases: ${Object.keys(listAgents()).join(', ') || 'none'}\n`); printUsage(); process.exit(2); } @@ -180,11 +361,29 @@ async function main() { process.exit(2); } + // Auto-detect protocol if not specified + if (!protocol) { + if (debug || !jsonOutput) { + console.error('šŸ” Auto-detecting protocol...'); + } + + try { + protocol = await detectProtocol(agentUrl); + if (debug || !jsonOutput) { + console.error(`āœ“ Detected protocol: ${protocol.toUpperCase()}\n`); + } + } catch (error) { + console.error(`ERROR: Failed to detect protocol: ${error.message}\n`); + console.error('Please specify protocol explicitly: adcp mcp or adcp a2a \n'); + process.exit(2); + } + } + if (debug) { console.error('DEBUG: Configuration'); console.error(` Protocol: ${protocol}`); console.error(` Agent URL: ${agentUrl}`); - console.error(` Tool: ${toolName}`); + console.error(` Tool: ${toolName || '(list tools)'}`); console.error(` Auth: ${authToken ? 'provided' : 'none'}`); console.error(` Payload: ${JSON.stringify(payload, null, 2)}`); console.error(''); @@ -317,16 +516,42 @@ async function main() { // Handle result if (result.success) { if (jsonOutput) { - // Raw JSON output - console.log(JSON.stringify(result.data, null, 2)); + // Raw JSON output - include protocol metadata + console.log(JSON.stringify({ + data: result.data, + metadata: { + taskId: result.metadata.taskId, + protocol: result.metadata.agent.protocol, + responseTimeMs: result.metadata.responseTimeMs, + ...(result.conversation && result.conversation.length > 0 && { + protocolMessage: extractProtocolMessage(result.conversation, result.metadata.agent.protocol), + contextId: result.metadata.taskId // Using taskId as context identifier + }) + } + }, null, 2)); } else { // Pretty output console.log('\nāœ… SUCCESS\n'); + + // Show protocol message if available + if (result.conversation && result.conversation.length > 0) { + const message = extractProtocolMessage(result.conversation, result.metadata.agent.protocol); + if (message) { + console.log('Protocol Message:'); + console.log(message); + console.log(''); + } + } + console.log('Response:'); console.log(JSON.stringify(result.data, null, 2)); console.log(''); + console.log(`Protocol: ${result.metadata.agent.protocol.toUpperCase()}`); console.log(`Response Time: ${result.metadata.responseTimeMs}ms`); console.log(`Task ID: ${result.metadata.taskId}`); + if (result.conversation && result.conversation.length > 0) { + console.log(`Context ID: ${result.metadata.taskId}`); + } } process.exit(0); } else { diff --git a/docs/CLI.md b/docs/CLI.md index ffb03056b..296bc81b3 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -1,6 +1,6 @@ # AdCP CLI Tool -A simple command-line utility for calling AdCP agents directly without writing code. +A simple command-line utility for calling AdCP agents directly without writing code. Features protocol auto-detection and agent alias management for quick access. ## Installation @@ -26,36 +26,38 @@ npx adcp [arguments...] ## Quick Start -### List Available Tools - -First, discover what tools/skills an agent supports: +### Save an Agent for Easy Access ```bash -adcp a2a https://test-agent.adcontextprotocol.org -adcp mcp https://agent.example.com/mcp +# Save agent with alias +adcp --save-auth test https://test-agent.adcontextprotocol.org +# Prompts for protocol (optional) and auth token (optional) + +# Now use the alias +adcp test get_products '{"brief":"coffee brands"}' ``` -### Call a Tool +### Direct URL Usage (No Setup Required) -Once you know what's available, call a specific tool: +Protocol auto-detection - no need to specify `mcp` or `a2a`: ```bash -adcp mcp https://agent.example.com/mcp get_products '{"brief":"coffee brands"}' +# List available tools +adcp https://test-agent.adcontextprotocol.org + +# Call a tool +adcp https://agent.example.com get_products '{"brief":"coffee brands"}' ``` ## Usage ``` -adcp [tool-name] [payload] [options] +adcp [tool-name] [payload] [options] ``` -### Required Arguments - -- **protocol**: Protocol to use (`mcp` or `a2a`) -- **agent-url**: Full URL to the agent endpoint - -### Optional Arguments +### Arguments +- **alias|url**: Saved agent alias (e.g., `test`) or full URL to agent endpoint - **tool-name**: Name of the AdCP tool/task to call (omit to list available tools) - **payload**: JSON payload for the tool (default: `{}`) - Inline JSON: `'{"brief":"text"}'` @@ -64,53 +66,107 @@ adcp [tool-name] [payload] [options] ### Options +- `--protocol PROTO`: Force protocol: `mcp` or `a2a` (default: auto-detect) - `--auth TOKEN`: Authentication token for the agent +- `--wait`: Wait for async/webhook responses (requires ngrok or --local) +- `--local`: Use local webhook without ngrok (for local agents only) +- `--timeout MS`: Webhook timeout in milliseconds (default: 300000 = 5min) - `--help, -h`: Show help message - `--json`: Output raw JSON response (default: pretty print) - `--debug`: Show debug information +### Agent Management Commands + +- `--save-auth [url] [protocol]`: Save agent configuration +- `--list-agents`: List all saved agents +- `--remove-agent `: Remove saved agent configuration +- `--show-config`: Show config file location + ## Examples +### Agent Alias Workflow + +```bash +# Save agents +adcp --save-auth test https://test-agent.adcontextprotocol.org +adcp --save-auth prod https://prod-agent.example.com + +# List saved agents +adcp --list-agents + +# Use aliases +adcp test +adcp test get_products '{"brief":"coffee brands"}' +adcp prod create_media_buy @payload.json +``` + ### List Available Tools/Skills Discover what an agent can do (no tool name = list tools): ```bash -# List MCP tools -adcp mcp https://agent.example.com/mcp +# Auto-detect protocol +adcp https://test-agent.adcontextprotocol.org +adcp test -# List A2A skills -adcp a2a https://test-agent.adcontextprotocol.org +# Explicit protocol (if needed) +adcp mcp https://agent.example.com/mcp +adcp a2a https://agent.example.com ``` Example output: ``` -šŸ“‹ Available A2A Skills +šŸ” Auto-detecting protocol... +āœ“ Detected protocol: MCP + +šŸ“‹ Agent Information + +Name: CLI Agent +Protocol: MCP +URL: https://agent.example.com/mcp -Agent: AdCP Sales Agent -Description: AI agent for programmatic advertising campaigns via AdCP protocol +Available Tools (14): 1. get_products - Browse available advertising products and inventory + Get available products matching the brief 2. create_media_buy - Create advertising campaigns with products, targeting, and budget + Create a media buy with the specified parameters 3. list_creative_formats - List all available creative formats and specifications + List all available creative formats ... ``` -### Basic Product Discovery (MCP) +### Basic Product Discovery ```bash -adcp mcp https://agent.example.com/mcp get_products '{"brief":"coffee subscription service","promoted_offering":"Premium coffee deliveries"}' +# With alias (auto-detect) +adcp test get_products '{"brief":"coffee subscription service"}' + +# With URL (auto-detect) +adcp https://agent.example.com get_products '{"brief":"coffee brands"}' + +# Force specific protocol +adcp https://agent.example.com get_products '{"brief":"coffee brands"}' --protocol mcp +adcp test list_authorized_properties --protocol a2a ``` -### With Authentication (A2A) +### Authentication Methods ```bash -adcp a2a https://agent.example.com list_creative_formats '{}' --auth your_token_here +# 1. Saved in agent config (recommended) +adcp --save-auth prod https://prod-agent.com +# Prompts for token securely + +adcp prod get_products '{"brief":"..."}' + +# 2. Explicit flag (overrides saved config) +adcp test get_products '{"brief":"..."}' --auth your_token_here + +# 3. Environment variable (fallback) +export ADCP_AUTH_TOKEN=your_token +adcp https://agent.example.com get_products '{"brief":"..."}' ``` ### From File @@ -120,7 +176,7 @@ Create a payload file: ```json { "brief": "Eco-friendly products for millennials", - "promoted_offering": "Sustainable consumer goods", + "brand_manifest": {"promoted_offering": "Sustainable consumer goods"}, "budget": 50000 } ``` @@ -128,13 +184,17 @@ Create a payload file: Then call: ```bash -adcp mcp https://agent.example.com/mcp get_products @payload.json --auth $AGENT_TOKEN +# With alias +adcp test get_products @payload.json + +# With URL +adcp https://agent.example.com get_products @payload.json ``` ### From Stdin ```bash -echo '{"brief":"travel packages"}' | adcp mcp https://agent.example.com/mcp get_products - +echo '{"brief":"travel packages"}' | adcp test get_products - ``` ### Create Media Buy diff --git a/scripts/install-hooks.js b/scripts/install-hooks.js index d9c2980e8..5002dd917 100644 --- a/scripts/install-hooks.js +++ b/scripts/install-hooks.js @@ -68,6 +68,11 @@ if [ ! -d "schemas/cache/latest" ]; then npm run sync-schemas fi +# Set CI=true to skip slow tests (matches GitHub Actions behavior) +# This skips tests marked with: skip: process.env.CI ? 'reason' : false +# e.g., error-scenarios.test.js which tests complex timeout/race conditions +export CI=true + # Run the comprehensive CI validation (includes schema validation) npm run ci:pre-push @@ -76,7 +81,7 @@ if [ $? -ne 0 ]; then echo "āŒ Pre-push validation failed!" echo "šŸ”§ Fix the issues above before pushing" echo "" - echo "šŸ’” To run validation manually: npm run ci:validate" + echo "šŸ’” To run validation manually: CI=true npm run ci:validate" echo "šŸ’” Schema issues? Try: npm run sync-schemas && npm run generate-types" echo "" exit 1 diff --git a/src/lib/core/ADCPMultiAgentClient.ts b/src/lib/core/ADCPMultiAgentClient.ts index 9a47aa0af..3ab5f8b98 100644 --- a/src/lib/core/ADCPMultiAgentClient.ts +++ b/src/lib/core/ADCPMultiAgentClient.ts @@ -574,6 +574,21 @@ export class ADCPMultiAgentClient { return this.agentClients.delete(agentId); } + /** + * Get individual agent client by ID + * + * @param agentId - ID of agent to retrieve + * @returns AgentClient instance + * @throws Error if agent not found + */ + getAgent(agentId: string): AgentClient { + const agent = this.agentClients.get(agentId); + if (!agent) { + throw new Error(`Agent '${agentId}' not found. Available agents: ${this.getAgentIds().join(', ')}`); + } + return agent; + } + /** * Check if an agent exists */ diff --git a/src/lib/core/AgentClient.ts b/src/lib/core/AgentClient.ts index 2c6fddf1f..3ad6f674d 100644 --- a/src/lib/core/AgentClient.ts +++ b/src/lib/core/AgentClient.ts @@ -436,6 +436,13 @@ export class AgentClient { return this.client.getProtocol(); } + /** + * Get agent information including capabilities + */ + async getAgentInfo() { + return this.client.getAgentInfo(); + } + /** * Check if there's an active conversation */ diff --git a/src/lib/index.ts b/src/lib/index.ts index 2404d26aa..57e1edf15 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -135,6 +135,7 @@ export * from './validation'; // ====== UTILITIES ====== export * from './utils'; export { getStandardFormats } from './utils'; +export { detectProtocol, detectProtocolWithTimeout } from './utils/protocol-detection'; // ====== LEGACY AGENT CLASSES ====== // Keep existing generated agent classes for backward compatibility diff --git a/src/lib/utils/protocol-detection.ts b/src/lib/utils/protocol-detection.ts new file mode 100644 index 000000000..3b92b034f --- /dev/null +++ b/src/lib/utils/protocol-detection.ts @@ -0,0 +1,92 @@ +/** + * Protocol Detection Utilities + * + * Auto-detects whether an agent endpoint uses MCP or A2A protocol + */ + +/** + * Detect protocol for a given agent URL + * + * Uses a hybrid approach: + * 1. Check URL patterns (fast heuristic) + * 2. Try A2A discovery endpoint (authoritative) + * 3. Default to MCP if A2A discovery fails + * + * @param url Agent URL to check + * @returns Promise resolving to 'a2a' or 'mcp' + */ +export async function detectProtocol(url: string): Promise<'a2a' | 'mcp'> { + // Step 1: Quick heuristic check + if (url.endsWith('/mcp/') || url.endsWith('/mcp')) { + return 'mcp'; + } + + // Step 2: Try A2A discovery + try { + const discoveryUrl = new URL('/.well-known/a2a-server', url); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); // 5s timeout + + const response = await fetch(discoveryUrl.toString(), { + method: 'GET', + signal: controller.signal, + headers: { + 'Accept': 'application/json' + } + }); + + clearTimeout(timeoutId); + + if (response.ok) { + return 'a2a'; + } + } catch (error) { + // Fetch failed - likely not A2A + } + + // Step 3: Default to MCP + return 'mcp'; +} + +/** + * Detect protocol with custom timeout + * + * @param url Agent URL to check + * @param timeoutMs Timeout in milliseconds (default: 5000) + * @returns Promise resolving to 'a2a' or 'mcp' + */ +export async function detectProtocolWithTimeout( + url: string, + timeoutMs: number = 5000 +): Promise<'a2a' | 'mcp'> { + // Quick heuristic check + if (url.endsWith('/mcp/') || url.endsWith('/mcp')) { + return 'mcp'; + } + + // Try A2A discovery with custom timeout + try { + const discoveryUrl = new URL('/.well-known/a2a-server', url); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + + const response = await fetch(discoveryUrl.toString(), { + method: 'GET', + signal: controller.signal, + headers: { + 'Accept': 'application/json' + } + }); + + clearTimeout(timeoutId); + + if (response.ok) { + return 'a2a'; + } + } catch (error) { + // Fetch failed - likely not A2A + } + + // Default to MCP + return 'mcp'; +} diff --git a/src/public/index.html b/src/public/index.html index 5e359ad4a..c67e833bf 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -2005,6 +2005,21 @@

Ad Context Protocol Testing Framework

+ + +