@adcp/client 2.3.2 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/bin/adcp-async-handler.js +273 -0
- package/bin/adcp.js +358 -0
- package/dist/lib/core/ADCPClient.d.ts +58 -2
- package/dist/lib/core/ADCPClient.d.ts.map +1 -1
- package/dist/lib/core/ADCPClient.js +200 -7
- package/dist/lib/core/ADCPClient.js.map +1 -1
- package/dist/lib/types/core.generated.d.ts +2 -2
- package/dist/lib/types/core.generated.d.ts.map +1 -1
- package/dist/lib/types/core.generated.js +1 -1
- package/dist/lib/types/schemas.generated.d.ts +2 -2
- package/dist/lib/types/schemas.generated.js +2 -2
- package/dist/lib/types/schemas.generated.js.map +1 -1
- package/dist/lib/types/tools.generated.d.ts +2 -2
- package/dist/lib/types/tools.generated.d.ts.map +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -455,6 +455,31 @@ WHERE operation_id = 'delivery_report_agent_x_2025-10'
|
|
|
455
455
|
ORDER BY sequence_number;
|
|
456
456
|
```
|
|
457
457
|
|
|
458
|
+
## CLI Tool
|
|
459
|
+
|
|
460
|
+
For development and testing, use the included CLI tool to interact with AdCP agents:
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
# List available tools from an agent
|
|
464
|
+
npx @adcp/client mcp https://test-agent.adcontextprotocol.org
|
|
465
|
+
|
|
466
|
+
# Call a tool with inline JSON payload
|
|
467
|
+
npx @adcp/client a2a https://agent.example.com get_products '{"brief":"Coffee brands"}'
|
|
468
|
+
|
|
469
|
+
# Use a file for payload
|
|
470
|
+
npx @adcp/client mcp https://agent.example.com create_media_buy @payload.json
|
|
471
|
+
|
|
472
|
+
# With authentication
|
|
473
|
+
npx @adcp/client mcp https://agent.example.com get_products '{"brief":"..."}' --auth your-token
|
|
474
|
+
|
|
475
|
+
# JSON output for scripting
|
|
476
|
+
npx @adcp/client mcp https://agent.example.com get_products '{"brief":"..."}' --json
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
**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.
|
|
480
|
+
|
|
481
|
+
See [docs/CLI.md](docs/CLI.md) for complete CLI documentation including webhook support for async operations.
|
|
482
|
+
|
|
458
483
|
## Testing
|
|
459
484
|
|
|
460
485
|
Try the live testing UI at `http://localhost:8080` when running the server:
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Async webhook handler for AdCP CLI
|
|
5
|
+
*
|
|
6
|
+
* This module handles async/webhook responses by:
|
|
7
|
+
* 1. Starting a temporary HTTP server for webhooks
|
|
8
|
+
* 2. Using ngrok to expose the server publicly (if available)
|
|
9
|
+
* 3. Waiting for the async response
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const http = require('http');
|
|
13
|
+
const { spawn } = require('child_process');
|
|
14
|
+
const { randomUUID } = require('crypto');
|
|
15
|
+
|
|
16
|
+
class AsyncWebhookHandler {
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this.port = options.port || 0; // 0 = random available port
|
|
19
|
+
this.timeout = options.timeout || 300000; // 5 minutes default
|
|
20
|
+
this.debug = options.debug || false;
|
|
21
|
+
this.server = null;
|
|
22
|
+
this.ngrokProcess = null;
|
|
23
|
+
this.webhookUrl = null;
|
|
24
|
+
this.operationId = randomUUID();
|
|
25
|
+
this.responsePromise = null;
|
|
26
|
+
this.responseResolver = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if ngrok is installed
|
|
31
|
+
*/
|
|
32
|
+
static isNgrokAvailable() {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const check = spawn('which', ['ngrok']);
|
|
35
|
+
check.on('close', (code) => resolve(code === 0));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Start the webhook server and optionally ngrok tunnel
|
|
41
|
+
* @param {boolean} useNgrok - Whether to use ngrok (default: true)
|
|
42
|
+
*/
|
|
43
|
+
async start(useNgrok = true) {
|
|
44
|
+
// Create the promise that will resolve when we get the webhook
|
|
45
|
+
this.responsePromise = new Promise((resolve, reject) => {
|
|
46
|
+
this.responseResolver = resolve;
|
|
47
|
+
this.responseRejector = reject;
|
|
48
|
+
|
|
49
|
+
// Set timeout
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
reject(new Error(`Webhook timeout after ${this.timeout}ms`));
|
|
52
|
+
}, this.timeout);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Start HTTP server
|
|
56
|
+
await this.startServer();
|
|
57
|
+
|
|
58
|
+
if (useNgrok) {
|
|
59
|
+
// Start ngrok tunnel
|
|
60
|
+
const ngrokAvailable = await AsyncWebhookHandler.isNgrokAvailable();
|
|
61
|
+
if (ngrokAvailable) {
|
|
62
|
+
await this.startNgrok();
|
|
63
|
+
} else {
|
|
64
|
+
throw new Error('ngrok is not installed. Install it with: brew install ngrok (Mac) or download from https://ngrok.com');
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
// Use local URL (for local agents)
|
|
68
|
+
this.webhookUrl = `http://localhost:${this.port}`;
|
|
69
|
+
|
|
70
|
+
if (this.debug) {
|
|
71
|
+
console.error(`✅ Local webhook server ready: ${this.webhookUrl}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return this.webhookUrl;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Start the HTTP server
|
|
80
|
+
*/
|
|
81
|
+
startServer() {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
this.server = http.createServer((req, res) => {
|
|
84
|
+
if (req.method === 'POST') {
|
|
85
|
+
let body = '';
|
|
86
|
+
|
|
87
|
+
req.on('data', chunk => {
|
|
88
|
+
body += chunk.toString();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
req.on('end', () => {
|
|
92
|
+
try {
|
|
93
|
+
const payload = JSON.parse(body);
|
|
94
|
+
|
|
95
|
+
if (this.debug) {
|
|
96
|
+
console.error('\n🎣 Webhook received:');
|
|
97
|
+
console.error(JSON.stringify(payload, null, 2));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Send 202 Accepted response
|
|
101
|
+
res.writeHead(202, { 'Content-Type': 'application/json' });
|
|
102
|
+
res.end(JSON.stringify({ status: 'accepted' }));
|
|
103
|
+
|
|
104
|
+
// Resolve the promise with the webhook payload
|
|
105
|
+
if (this.responseResolver) {
|
|
106
|
+
this.responseResolver(payload);
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (this.debug) {
|
|
110
|
+
console.error('Error parsing webhook:', error);
|
|
111
|
+
}
|
|
112
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
113
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
// Health check endpoint
|
|
118
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
119
|
+
res.end(JSON.stringify({ status: 'ready', operation_id: this.operationId }));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
this.server.listen(this.port, () => {
|
|
124
|
+
const address = this.server.address();
|
|
125
|
+
this.port = address.port;
|
|
126
|
+
|
|
127
|
+
if (this.debug) {
|
|
128
|
+
console.error(`✅ Webhook server listening on port ${this.port}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
resolve();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
this.server.on('error', reject);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Start ngrok tunnel
|
|
140
|
+
*/
|
|
141
|
+
startNgrok() {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
if (this.debug) {
|
|
144
|
+
console.error(`🚇 Starting ngrok tunnel for port ${this.port}...`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Start ngrok with JSON output for easier parsing
|
|
148
|
+
this.ngrokProcess = spawn('ngrok', ['http', String(this.port), '--log=stdout', '--log-format=json']);
|
|
149
|
+
|
|
150
|
+
let ngrokStarted = false;
|
|
151
|
+
let buffer = '';
|
|
152
|
+
|
|
153
|
+
this.ngrokProcess.stdout.on('data', (data) => {
|
|
154
|
+
buffer += data.toString();
|
|
155
|
+
const lines = buffer.split('\n');
|
|
156
|
+
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
157
|
+
|
|
158
|
+
for (const line of lines) {
|
|
159
|
+
if (!line.trim()) continue;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const parsed = JSON.parse(line);
|
|
163
|
+
|
|
164
|
+
// Look for the tunnel URL in ngrok's JSON output
|
|
165
|
+
if (parsed.url && parsed.url.startsWith('https://')) {
|
|
166
|
+
this.webhookUrl = parsed.url;
|
|
167
|
+
ngrokStarted = true;
|
|
168
|
+
|
|
169
|
+
if (this.debug) {
|
|
170
|
+
console.error(`✅ ngrok tunnel ready: ${this.webhookUrl}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
resolve();
|
|
174
|
+
}
|
|
175
|
+
} catch (e) {
|
|
176
|
+
// Not JSON, might be plain text output
|
|
177
|
+
// Try to extract URL from plain text
|
|
178
|
+
const urlMatch = line.match(/https:\/\/[a-z0-9-]+\.ngrok(?:-free)?\.(?:app|io)/);
|
|
179
|
+
if (urlMatch && !ngrokStarted) {
|
|
180
|
+
this.webhookUrl = urlMatch[0];
|
|
181
|
+
ngrokStarted = true;
|
|
182
|
+
|
|
183
|
+
if (this.debug) {
|
|
184
|
+
console.error(`✅ ngrok tunnel ready: ${this.webhookUrl}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
resolve();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
this.ngrokProcess.stderr.on('data', (data) => {
|
|
194
|
+
if (this.debug) {
|
|
195
|
+
console.error('ngrok stderr:', data.toString());
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
this.ngrokProcess.on('error', (error) => {
|
|
200
|
+
reject(new Error(`Failed to start ngrok: ${error.message}`));
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
this.ngrokProcess.on('close', (code) => {
|
|
204
|
+
if (!ngrokStarted && code !== 0) {
|
|
205
|
+
reject(new Error(`ngrok exited with code ${code}`));
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Timeout for ngrok startup
|
|
210
|
+
setTimeout(() => {
|
|
211
|
+
if (!ngrokStarted) {
|
|
212
|
+
reject(new Error('ngrok failed to start within 10 seconds'));
|
|
213
|
+
}
|
|
214
|
+
}, 10000);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Wait for the webhook response
|
|
220
|
+
*/
|
|
221
|
+
async waitForResponse() {
|
|
222
|
+
if (this.debug) {
|
|
223
|
+
console.error('\n⏳ Waiting for async response...');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const startTime = Date.now();
|
|
227
|
+
const result = await this.responsePromise;
|
|
228
|
+
const duration = Date.now() - startTime;
|
|
229
|
+
|
|
230
|
+
if (this.debug) {
|
|
231
|
+
console.error(`✅ Response received after ${(duration / 1000).toFixed(1)}s\n`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Clean up resources
|
|
239
|
+
*/
|
|
240
|
+
async cleanup() {
|
|
241
|
+
if (this.debug) {
|
|
242
|
+
console.error('🧹 Cleaning up...');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Close HTTP server
|
|
246
|
+
if (this.server) {
|
|
247
|
+
await new Promise((resolve) => {
|
|
248
|
+
this.server.close(() => resolve());
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Kill ngrok process
|
|
253
|
+
if (this.ngrokProcess) {
|
|
254
|
+
this.ngrokProcess.kill();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (this.debug) {
|
|
258
|
+
console.error('✅ Cleanup complete');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Get the webhook URL with operation ID
|
|
264
|
+
*/
|
|
265
|
+
getWebhookUrl() {
|
|
266
|
+
if (!this.webhookUrl) {
|
|
267
|
+
throw new Error('Webhook server not started');
|
|
268
|
+
}
|
|
269
|
+
return `${this.webhookUrl}?operation_id=${this.operationId}`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = { AsyncWebhookHandler };
|
package/bin/adcp.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AdCP CLI Tool
|
|
5
|
+
*
|
|
6
|
+
* Simple command-line utility to call AdCP agents directly
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* adcp <protocol> <agent-url> <tool-name> [payload-json] [--auth token]
|
|
10
|
+
*
|
|
11
|
+
* Examples:
|
|
12
|
+
* adcp mcp https://agent.example.com/mcp get_products '{"brief":"coffee brands"}'
|
|
13
|
+
* adcp a2a https://agent.example.com list_creative_formats '{}' --auth your_token_here
|
|
14
|
+
* adcp mcp https://agent.example.com/mcp create_media_buy @payload.json --auth $AGENT_TOKEN
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { ADCPClient } = require('../dist/lib/index.js');
|
|
18
|
+
const { readFileSync } = require('fs');
|
|
19
|
+
const { AsyncWebhookHandler } = require('./adcp-async-handler.js');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Display agent info - just calls library method
|
|
23
|
+
*/
|
|
24
|
+
async function displayAgentInfo(agentConfig, jsonOutput) {
|
|
25
|
+
const client = new ADCPClient(agentConfig);
|
|
26
|
+
const info = await client.getAgentInfo();
|
|
27
|
+
|
|
28
|
+
if (jsonOutput) {
|
|
29
|
+
console.log(JSON.stringify(info, null, 2));
|
|
30
|
+
} else {
|
|
31
|
+
console.log(`\n📋 Agent Information\n`);
|
|
32
|
+
console.log(`Name: ${info.name}`);
|
|
33
|
+
if (info.description) {
|
|
34
|
+
console.log(`Description: ${info.description}`);
|
|
35
|
+
}
|
|
36
|
+
console.log(`Protocol: ${info.protocol.toUpperCase()}`);
|
|
37
|
+
console.log(`URL: ${info.url}`);
|
|
38
|
+
console.log(`\nAvailable Tools (${info.tools.length}):\n`);
|
|
39
|
+
|
|
40
|
+
if (info.tools.length === 0) {
|
|
41
|
+
console.log('No tools found.');
|
|
42
|
+
} else {
|
|
43
|
+
info.tools.forEach((tool, i) => {
|
|
44
|
+
console.log(`${i + 1}. ${tool.name}`);
|
|
45
|
+
if (tool.description) {
|
|
46
|
+
console.log(` ${tool.description}`);
|
|
47
|
+
}
|
|
48
|
+
if (tool.parameters && tool.parameters.length > 0) {
|
|
49
|
+
console.log(` Parameters: ${tool.parameters.join(', ')}`);
|
|
50
|
+
}
|
|
51
|
+
console.log('');
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function printUsage() {
|
|
58
|
+
console.log(`
|
|
59
|
+
AdCP CLI Tool - Direct Agent Communication
|
|
60
|
+
|
|
61
|
+
USAGE:
|
|
62
|
+
adcp <protocol> <agent-url> [tool-name] [payload] [options]
|
|
63
|
+
|
|
64
|
+
ARGUMENTS:
|
|
65
|
+
protocol Protocol to use: 'mcp' or 'a2a'
|
|
66
|
+
agent-url Full URL to the agent endpoint
|
|
67
|
+
tool-name Name of the tool to call (optional - omit to list available tools)
|
|
68
|
+
payload JSON payload for the tool (default: {})
|
|
69
|
+
- Can be inline JSON: '{"brief":"text"}'
|
|
70
|
+
- Can be file path: @payload.json
|
|
71
|
+
- Can be stdin: -
|
|
72
|
+
|
|
73
|
+
OPTIONS:
|
|
74
|
+
--auth TOKEN Authentication token for the agent
|
|
75
|
+
--wait Wait for async/webhook responses (requires ngrok or --local)
|
|
76
|
+
--local Use local webhook without ngrok (for local agents only)
|
|
77
|
+
--timeout MS Webhook timeout in milliseconds (default: 300000 = 5min)
|
|
78
|
+
--help, -h Show this help message
|
|
79
|
+
--json Output raw JSON response (default: pretty print)
|
|
80
|
+
--debug Show debug information
|
|
81
|
+
|
|
82
|
+
EXAMPLES:
|
|
83
|
+
# List available tools
|
|
84
|
+
adcp mcp https://agent.example.com/mcp
|
|
85
|
+
adcp a2a https://creative.adcontextprotocol.org
|
|
86
|
+
|
|
87
|
+
# Simple product discovery
|
|
88
|
+
adcp mcp https://agent.example.com/mcp get_products '{"brief":"coffee brands"}'
|
|
89
|
+
|
|
90
|
+
# With authentication
|
|
91
|
+
adcp a2a https://agent.example.com list_creative_formats '{}' --auth your_token
|
|
92
|
+
|
|
93
|
+
# Wait for async response (requires ngrok)
|
|
94
|
+
adcp mcp https://agent.example.com/mcp create_media_buy @payload.json --auth $TOKEN --wait
|
|
95
|
+
|
|
96
|
+
# Wait for async response from local agent (no ngrok needed)
|
|
97
|
+
adcp mcp http://localhost:3000/mcp create_media_buy @payload.json --wait --local
|
|
98
|
+
|
|
99
|
+
# From file
|
|
100
|
+
adcp mcp https://agent.example.com/mcp create_media_buy @payload.json --auth $TOKEN
|
|
101
|
+
|
|
102
|
+
# From stdin
|
|
103
|
+
echo '{"brief":"travel"}' | adcp mcp https://agent.example.com/mcp get_products -
|
|
104
|
+
|
|
105
|
+
ENVIRONMENT VARIABLES:
|
|
106
|
+
ADCP_AUTH_TOKEN Default authentication token (overridden by --auth)
|
|
107
|
+
ADCP_DEBUG Enable debug mode (set to 'true')
|
|
108
|
+
|
|
109
|
+
EXIT CODES:
|
|
110
|
+
0 Success
|
|
111
|
+
1 General error
|
|
112
|
+
2 Invalid arguments
|
|
113
|
+
3 Agent error
|
|
114
|
+
`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function main() {
|
|
118
|
+
const args = process.argv.slice(2);
|
|
119
|
+
|
|
120
|
+
// Handle help
|
|
121
|
+
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
|
122
|
+
printUsage();
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Parse arguments
|
|
127
|
+
if (args.length < 2) {
|
|
128
|
+
console.error('ERROR: Missing required arguments\n');
|
|
129
|
+
printUsage();
|
|
130
|
+
process.exit(2);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Parse options first
|
|
134
|
+
const authIndex = args.indexOf('--auth');
|
|
135
|
+
const authToken = authIndex !== -1 ? args[authIndex + 1] : process.env.ADCP_AUTH_TOKEN;
|
|
136
|
+
const jsonOutput = args.includes('--json');
|
|
137
|
+
const debug = args.includes('--debug') || process.env.ADCP_DEBUG === 'true';
|
|
138
|
+
const waitForAsync = args.includes('--wait');
|
|
139
|
+
const useLocalWebhook = args.includes('--local');
|
|
140
|
+
const timeoutIndex = args.indexOf('--timeout');
|
|
141
|
+
const timeout = timeoutIndex !== -1 ? parseInt(args[timeoutIndex + 1]) : 300000;
|
|
142
|
+
|
|
143
|
+
// Filter out flag arguments to find positional arguments
|
|
144
|
+
const positionalArgs = args.filter(arg =>
|
|
145
|
+
!arg.startsWith('--') &&
|
|
146
|
+
arg !== authToken && // Don't include the auth token value
|
|
147
|
+
arg !== (timeoutIndex !== -1 ? args[timeoutIndex + 1] : null) // Don't include timeout value
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const protocol = positionalArgs[0];
|
|
151
|
+
const agentUrl = positionalArgs[1];
|
|
152
|
+
const toolName = positionalArgs[2]; // Optional - if not provided, list tools
|
|
153
|
+
let payloadArg = positionalArgs[3] || '{}';
|
|
154
|
+
|
|
155
|
+
// Validate protocol
|
|
156
|
+
if (protocol !== 'mcp' && protocol !== 'a2a') {
|
|
157
|
+
console.error(`ERROR: Invalid protocol '${protocol}'. Must be 'mcp' or 'a2a'\n`);
|
|
158
|
+
printUsage();
|
|
159
|
+
process.exit(2);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Parse payload
|
|
163
|
+
let payload;
|
|
164
|
+
try {
|
|
165
|
+
if (payloadArg === '-') {
|
|
166
|
+
// Read from stdin
|
|
167
|
+
const stdin = readFileSync(0, 'utf-8');
|
|
168
|
+
payload = JSON.parse(stdin);
|
|
169
|
+
} else if (payloadArg.startsWith('@')) {
|
|
170
|
+
// Read from file
|
|
171
|
+
const filePath = payloadArg.substring(1);
|
|
172
|
+
const fileContent = readFileSync(filePath, 'utf-8');
|
|
173
|
+
payload = JSON.parse(fileContent);
|
|
174
|
+
} else {
|
|
175
|
+
// Parse inline JSON
|
|
176
|
+
payload = JSON.parse(payloadArg);
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(`ERROR: Invalid JSON payload: ${error.message}\n`);
|
|
180
|
+
process.exit(2);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (debug) {
|
|
184
|
+
console.error('DEBUG: Configuration');
|
|
185
|
+
console.error(` Protocol: ${protocol}`);
|
|
186
|
+
console.error(` Agent URL: ${agentUrl}`);
|
|
187
|
+
console.error(` Tool: ${toolName}`);
|
|
188
|
+
console.error(` Auth: ${authToken ? 'provided' : 'none'}`);
|
|
189
|
+
console.error(` Payload: ${JSON.stringify(payload, null, 2)}`);
|
|
190
|
+
console.error('');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Create agent config
|
|
194
|
+
const agentConfig = {
|
|
195
|
+
id: 'cli-agent',
|
|
196
|
+
name: 'CLI Agent',
|
|
197
|
+
agent_uri: agentUrl,
|
|
198
|
+
protocol: protocol,
|
|
199
|
+
...(authToken && { auth_token_env: authToken })
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
// If no tool name provided, display agent info
|
|
204
|
+
if (!toolName) {
|
|
205
|
+
if (debug) {
|
|
206
|
+
console.error('DEBUG: No tool specified, displaying agent info...\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
await displayAgentInfo(agentConfig, jsonOutput);
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Set up webhook handler if --wait flag is used
|
|
214
|
+
let webhookHandler = null;
|
|
215
|
+
let webhookUrl = null;
|
|
216
|
+
|
|
217
|
+
if (waitForAsync) {
|
|
218
|
+
const useNgrok = !useLocalWebhook;
|
|
219
|
+
|
|
220
|
+
// Check if ngrok is available (unless using --local)
|
|
221
|
+
if (useNgrok) {
|
|
222
|
+
const ngrokAvailable = await AsyncWebhookHandler.isNgrokAvailable();
|
|
223
|
+
if (!ngrokAvailable) {
|
|
224
|
+
console.error('\n❌ ERROR: --wait flag requires ngrok to be installed\n');
|
|
225
|
+
console.error('Install ngrok:');
|
|
226
|
+
console.error(' Mac: brew install ngrok');
|
|
227
|
+
console.error(' Windows: choco install ngrok');
|
|
228
|
+
console.error(' Linux: Download from https://ngrok.com/download');
|
|
229
|
+
console.error('\nOr use --local flag for local agents (e.g., http://localhost:3000)\n');
|
|
230
|
+
process.exit(2);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (debug) {
|
|
235
|
+
console.error(`DEBUG: Setting up ${useNgrok ? 'ngrok' : 'local'} webhook handler...\n`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
webhookHandler = new AsyncWebhookHandler({
|
|
239
|
+
timeout: timeout,
|
|
240
|
+
debug: debug
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
webhookUrl = await webhookHandler.start(useNgrok);
|
|
245
|
+
|
|
246
|
+
if (!jsonOutput) {
|
|
247
|
+
console.log(`\n🌐 ${useNgrok ? 'Public webhook' : 'Local webhook'} endpoint ready`);
|
|
248
|
+
console.log(` URL: ${webhookUrl}`);
|
|
249
|
+
console.log(` Timeout: ${timeout / 1000}s`);
|
|
250
|
+
if (useLocalWebhook) {
|
|
251
|
+
console.log(` ⚠️ Local mode: Agent must be accessible at localhost`);
|
|
252
|
+
}
|
|
253
|
+
console.log('');
|
|
254
|
+
}
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error('\n❌ ERROR: Failed to start webhook handler\n');
|
|
257
|
+
console.error(error.message);
|
|
258
|
+
if (debug) {
|
|
259
|
+
console.error('\nStack trace:');
|
|
260
|
+
console.error(error.stack);
|
|
261
|
+
}
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Create ADCP client with optional webhook configuration
|
|
267
|
+
const client = new ADCPClient(agentConfig, {
|
|
268
|
+
debug: debug,
|
|
269
|
+
...(webhookUrl && {
|
|
270
|
+
webhookUrlTemplate: webhookUrl,
|
|
271
|
+
webhookSecret: 'cli-webhook-secret'
|
|
272
|
+
})
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
if (debug) {
|
|
276
|
+
console.error('DEBUG: Executing task...\n');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Execute the task
|
|
280
|
+
const result = await client.executeTask(toolName, payload);
|
|
281
|
+
|
|
282
|
+
// If waiting for async response, handle webhook
|
|
283
|
+
if (waitForAsync && webhookHandler) {
|
|
284
|
+
if (result.status === 'submitted' || result.status === 'working') {
|
|
285
|
+
if (!jsonOutput) {
|
|
286
|
+
console.log('📤 Task submitted, waiting for async response...');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const webhookResponse = await webhookHandler.waitForResponse();
|
|
291
|
+
|
|
292
|
+
// Clean up webhook handler
|
|
293
|
+
await webhookHandler.cleanup();
|
|
294
|
+
|
|
295
|
+
// Output webhook response
|
|
296
|
+
if (jsonOutput) {
|
|
297
|
+
console.log(JSON.stringify(webhookResponse.result || webhookResponse, null, 2));
|
|
298
|
+
} else {
|
|
299
|
+
console.log('\n✅ ASYNC RESPONSE RECEIVED\n');
|
|
300
|
+
console.log('Response:');
|
|
301
|
+
console.log(JSON.stringify(webhookResponse.result || webhookResponse, null, 2));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
process.exit(0);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
await webhookHandler.cleanup();
|
|
307
|
+
console.error('\n❌ WEBHOOK TIMEOUT\n');
|
|
308
|
+
console.error(error.message);
|
|
309
|
+
process.exit(3);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
// Task completed synchronously, clean up webhook
|
|
313
|
+
await webhookHandler.cleanup();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Handle result
|
|
318
|
+
if (result.success) {
|
|
319
|
+
if (jsonOutput) {
|
|
320
|
+
// Raw JSON output
|
|
321
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
322
|
+
} else {
|
|
323
|
+
// Pretty output
|
|
324
|
+
console.log('\n✅ SUCCESS\n');
|
|
325
|
+
console.log('Response:');
|
|
326
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
327
|
+
console.log('');
|
|
328
|
+
console.log(`Response Time: ${result.metadata.responseTimeMs}ms`);
|
|
329
|
+
console.log(`Task ID: ${result.metadata.taskId}`);
|
|
330
|
+
}
|
|
331
|
+
process.exit(0);
|
|
332
|
+
} else {
|
|
333
|
+
console.error('\n❌ TASK FAILED\n');
|
|
334
|
+
console.error(`Error: ${result.error || 'Unknown error'}`);
|
|
335
|
+
if (result.metadata?.clarificationRounds) {
|
|
336
|
+
console.error(`Clarifications: ${result.metadata.clarificationRounds}`);
|
|
337
|
+
}
|
|
338
|
+
if (debug && result.metadata) {
|
|
339
|
+
console.error('\nMetadata:');
|
|
340
|
+
console.error(JSON.stringify(result.metadata, null, 2));
|
|
341
|
+
}
|
|
342
|
+
process.exit(3);
|
|
343
|
+
}
|
|
344
|
+
} catch (error) {
|
|
345
|
+
console.error('\n❌ ERROR\n');
|
|
346
|
+
console.error(error.message);
|
|
347
|
+
if (debug) {
|
|
348
|
+
console.error('\nStack trace:');
|
|
349
|
+
console.error(error.stack);
|
|
350
|
+
}
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
main().catch(error => {
|
|
356
|
+
console.error('FATAL ERROR:', error.message);
|
|
357
|
+
process.exit(1);
|
|
358
|
+
});
|