@agentvalet/mcp-server 0.2.5 → 0.3.0

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/dist/index.js CHANGED
@@ -54,7 +54,7 @@ const LIST_PLATFORMS_TOOL = {
54
54
  };
55
55
  const USE_PLATFORM_TOOL = {
56
56
  name: "use_platform",
57
- description: "use_platform: Call an external platform API (Airtable, GitHub, Slack, etc.) through the AgentValet proxy.\nInput: platform (string), endpoint (string), method (GET|POST|PUT|PATCH|DELETE), scope (string), data (object, optional).\nReturns: upstream API response body.\nAuth: Bearer JWT.",
57
+ description: "use_platform: Call an external platform API (Airtable, GitHub, Slack, etc.) through the AgentValet proxy.\nInput: platform (string), endpoint (string), method (GET|POST|PUT|PATCH|DELETE), scope (string), data (object, optional).\nReturns: upstream API response body. May take up to 50 seconds when the action requires owner approval — the call will block while we wait, then return the approved result transparently. If approval doesn't land in time, returns a `pending_approval` envelope and the action runs asynchronously; the user is notified when it completes.\nAuth: Bearer JWT.",
58
58
  inputSchema: {
59
59
  type: "object",
60
60
  properties: {
@@ -221,13 +221,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
221
221
  isError: true,
222
222
  };
223
223
  }
224
+ const progressToken = request.params._meta?.progressToken;
224
225
  return await handleUsePlatform({
225
226
  platform: args.platform,
226
227
  endpoint: args.endpoint,
227
228
  method: args.method,
228
229
  scope: args.scope,
229
230
  data: args.data,
230
- });
231
+ }, progressToken);
231
232
  }
232
233
  if (name === "agent_register") {
233
234
  if (!args || typeof args.owner_id !== "string" || typeof args.agent_name !== "string" || !Array.isArray(args.requested_scopes)) {
@@ -304,7 +305,13 @@ async function handleListPlatforms() {
304
305
  return errorContent(`Proxy error ${response.status}: ${body}`);
305
306
  return { content: [{ type: "text", text: body }] };
306
307
  }
307
- async function handleUsePlatform(params) {
308
+ // Long-poll budget — under Claude Desktop's hardcoded 60s tool timeout
309
+ // (with 10s of safety). After this we return a graceful "queued" message and
310
+ // the action lands in the user's pending-actions list (Layer 4).
311
+ const APPROVAL_POLL_BUDGET_MS = 50_000;
312
+ const APPROVAL_POLL_INTERVAL_MS = 2_000;
313
+ const APPROVAL_PROGRESS_INTERVAL_MS = 5_000;
314
+ async function handleUsePlatform(params, progressToken) {
308
315
  if (AGENT_PRIVATE_KEY_RAW === null) {
309
316
  await notifyBindSecret();
310
317
  return pendingFirstCallResponse();
@@ -327,10 +334,112 @@ async function handleUsePlatform(params) {
327
334
  return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
328
335
  }
329
336
  const body = await response.text();
337
+ // 202 + approval_id → enter long-poll. The proxy returns this when the
338
+ // owner needs to approve the call. We poll /v1/approvals/:id every ~2s
339
+ // for up to 50s; if the owner approves in time the proxy re-runs the
340
+ // upstream call and we return its result to the agent transparently.
341
+ if (response.status === 202) {
342
+ const parsed = safeJsonParse(body);
343
+ const approvalId = parsed?.approval_id;
344
+ if (approvalId) {
345
+ return await waitForApproval(approvalId, params, progressToken);
346
+ }
347
+ }
330
348
  if (!response.ok)
331
349
  return errorContent(`Proxy error ${response.status}: ${body}`);
332
350
  return { content: [{ type: "text", text: body }] };
333
351
  }
352
+ async function waitForApproval(approvalId, originalCall, progressToken) {
353
+ const startedAt = Date.now();
354
+ let lastProgressAt = 0;
355
+ // Initial progress so the user sees activity immediately.
356
+ await sendProgress(progressToken, 0, APPROVAL_POLL_BUDGET_MS, `Owner approval required for ${originalCall.platform}:${originalCall.scope} — waiting…`);
357
+ while (Date.now() - startedAt < APPROVAL_POLL_BUDGET_MS) {
358
+ await sleep(APPROVAL_POLL_INTERVAL_MS);
359
+ const elapsed = Date.now() - startedAt;
360
+ let res;
361
+ try {
362
+ res = await fetchWithAuth(`${PROXY_URL}/v1/approvals/${approvalId}`, { method: "GET" });
363
+ }
364
+ catch {
365
+ // Transient network error — try again next tick.
366
+ continue;
367
+ }
368
+ if (!res.ok) {
369
+ // 404/410 etc. — treat as terminal. Surface back to the agent.
370
+ const text = await res.text();
371
+ return errorContent(`Approval status error ${res.status}: ${text}`);
372
+ }
373
+ const status = (await res.json());
374
+ if (status.status === "approved" && status.executed_at && status.result) {
375
+ // Re-execution complete. Return the upstream response transparently —
376
+ // the agent sees this as if the original use_platform call just succeeded.
377
+ const r = status.result;
378
+ const summary = `[Owner approved after ${Math.round(elapsed / 1000)}s]\n` +
379
+ JSON.stringify(r.data ?? null);
380
+ // Mirror the non-2xx-from-upstream behaviour of the regular path.
381
+ if (r.status < 200 || r.status >= 300) {
382
+ return errorContent(`Upstream returned ${r.status}: ${JSON.stringify(r.data)}`);
383
+ }
384
+ return { content: [{ type: "text", text: summary }] };
385
+ }
386
+ if (status.status === "denied") {
387
+ return errorContent(`Owner denied this action${status.execution_error ? `: ${status.execution_error}` : "."}`);
388
+ }
389
+ if (status.status === "expired") {
390
+ return errorContent("Approval request expired before the owner responded.");
391
+ }
392
+ if (status.execution_error) {
393
+ // Approved but re-execution failed (network / upstream / agent revoked).
394
+ return errorContent(`Approved but re-execution failed: ${status.execution_error}`);
395
+ }
396
+ // Still pending — emit a progress update at most once per ~5s so the
397
+ // user can see the wait advancing in the Claude Desktop tool UI.
398
+ if (Date.now() - lastProgressAt >= APPROVAL_PROGRESS_INTERVAL_MS) {
399
+ await sendProgress(progressToken, elapsed, APPROVAL_POLL_BUDGET_MS, `Waiting for owner approval — ${Math.round(elapsed / 1000)}s elapsed`);
400
+ lastProgressAt = Date.now();
401
+ }
402
+ }
403
+ // Timed out — fall through to async-recap path. The action is still
404
+ // queued and will run when the owner approves; the user is notified via
405
+ // push/email at that point. Layer 4's list_my_pending_actions surfaces it.
406
+ return {
407
+ content: [{
408
+ type: "text",
409
+ text: JSON.stringify({
410
+ status: "pending_approval",
411
+ approval_id: approvalId,
412
+ message: `Owner hasn't approved within ${APPROVAL_POLL_BUDGET_MS / 1000}s. ` +
413
+ `The action is queued — your owner will be notified, and you'll see it ` +
414
+ `complete next time we chat (or you can ask me to check pending actions).`,
415
+ }),
416
+ }],
417
+ };
418
+ }
419
+ function safeJsonParse(text) {
420
+ try {
421
+ return JSON.parse(text);
422
+ }
423
+ catch {
424
+ return null;
425
+ }
426
+ }
427
+ function sleep(ms) {
428
+ return new Promise((resolve) => setTimeout(resolve, ms));
429
+ }
430
+ async function sendProgress(token, progress, total, message) {
431
+ if (token === undefined)
432
+ return;
433
+ try {
434
+ await server.notification({
435
+ method: "notifications/progress",
436
+ params: { progressToken: token, progress, total, message },
437
+ });
438
+ }
439
+ catch {
440
+ // Best-effort — never let a failed notification break the call.
441
+ }
442
+ }
334
443
  async function handleAgentRegister(args) {
335
444
  let response;
336
445
  try {
@@ -11,7 +11,8 @@ Tool selection:
11
11
  3. Call \`use_platform\` with the exact platform, endpoint, method, and scope returned by \`list_platforms\`.
12
12
 
13
13
  Response handling:
14
- - If \`use_platform\` returns \`pending_approval\`, waitdo not retry. The owner will approve out of band.
14
+ - \`use_platform\` now waits up to 50 seconds for owner approval automatically. Most approvals complete in this window the call simply takes longer and returns the upstream result like a normal success. Tell the user "waiting for owner approval" if the call is taking more than ~5 seconds.
15
+ - If \`use_platform\` returns a \`pending_approval\` envelope after that wait, the action is queued and will run asynchronously when the owner approves. Tell the user clearly. Do NOT retry — duplicate calls will queue duplicate approvals.
15
16
  - Do not retry a denied call with a different scope.
16
17
  - If a \`use_platform\` error response includes a \`report_hint\` block, you may briefly ask the user "Want me to lodge this with your AgentValet owner?" — on yes, call \`report_self_diagnostic\` with a one-sentence narrative plus the \`correlation_id\` from the hint so the owner can investigate.
17
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvalet/mcp-server",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "AgentValet MCP server — lets AI agents call approved platforms via the AgentValet proxy",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",