@agentvalet/mcp-server 0.2.5 → 0.3.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/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: {
@@ -184,6 +184,11 @@ const REPORT_SELF_DIAGNOSTIC_TOOL = {
184
184
  required: ["severity", "message"],
185
185
  },
186
186
  };
187
+ const LIST_MY_PENDING_ACTIONS_TOOL = {
188
+ name: "list_my_pending_actions",
189
+ description: "list_my_pending_actions: Returns this agent's currently-pending approval requests AND any that completed in the last 24 hours. Use this at session start when the user mentions an earlier action, or when use_platform's long-poll timed out and the user comes back asking what happened.\nInput: None.\nReturns: { pending: [{approval_id, platform_id, scope, created_at, expires_at}], recently_completed: [{approval_id, platform_id, scope, status, executed_at, result_summary, execution_error}] }.\nAuth: Bearer agent JWT (sent automatically).",
190
+ inputSchema: { type: "object", properties: {} },
191
+ };
187
192
  // TODO: intent_resolve tool — planned for future release
188
193
  // ---------------------------------------------------------------------------
189
194
  // MCP server setup
@@ -197,6 +202,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
197
202
  AGENT_STATUS_TOOL,
198
203
  AUTHZEN_EVALUATE_TOOL,
199
204
  REPORT_SELF_DIAGNOSTIC_TOOL,
205
+ LIST_MY_PENDING_ACTIONS_TOOL,
200
206
  ],
201
207
  }));
202
208
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -221,13 +227,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
221
227
  isError: true,
222
228
  };
223
229
  }
230
+ const progressToken = request.params._meta?.progressToken;
224
231
  return await handleUsePlatform({
225
232
  platform: args.platform,
226
233
  endpoint: args.endpoint,
227
234
  method: args.method,
228
235
  scope: args.scope,
229
236
  data: args.data,
230
- });
237
+ }, progressToken);
231
238
  }
232
239
  if (name === "agent_register") {
233
240
  if (!args || typeof args.owner_id !== "string" || typeof args.agent_name !== "string" || !Array.isArray(args.requested_scopes)) {
@@ -247,6 +254,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
247
254
  }
248
255
  return await handleAuthzenEvaluate(args.platform_id, args.scope);
249
256
  }
257
+ if (name === "list_my_pending_actions") {
258
+ return await handleListMyPendingActions();
259
+ }
250
260
  if (name === "report_self_diagnostic") {
251
261
  if (!args || typeof args.severity !== "string" || typeof args.message !== "string") {
252
262
  return errorContent("Invalid or missing arguments: severity and message are required");
@@ -304,7 +314,13 @@ async function handleListPlatforms() {
304
314
  return errorContent(`Proxy error ${response.status}: ${body}`);
305
315
  return { content: [{ type: "text", text: body }] };
306
316
  }
307
- async function handleUsePlatform(params) {
317
+ // Long-poll budget — under Claude Desktop's hardcoded 60s tool timeout
318
+ // (with 10s of safety). After this we return a graceful "queued" message and
319
+ // the action lands in the user's pending-actions list (Layer 4).
320
+ const APPROVAL_POLL_BUDGET_MS = 50_000;
321
+ const APPROVAL_POLL_INTERVAL_MS = 2_000;
322
+ const APPROVAL_PROGRESS_INTERVAL_MS = 5_000;
323
+ async function handleUsePlatform(params, progressToken) {
308
324
  if (AGENT_PRIVATE_KEY_RAW === null) {
309
325
  await notifyBindSecret();
310
326
  return pendingFirstCallResponse();
@@ -327,10 +343,112 @@ async function handleUsePlatform(params) {
327
343
  return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
328
344
  }
329
345
  const body = await response.text();
346
+ // 202 + approval_id → enter long-poll. The proxy returns this when the
347
+ // owner needs to approve the call. We poll /v1/approvals/:id every ~2s
348
+ // for up to 50s; if the owner approves in time the proxy re-runs the
349
+ // upstream call and we return its result to the agent transparently.
350
+ if (response.status === 202) {
351
+ const parsed = safeJsonParse(body);
352
+ const approvalId = parsed?.approval_id;
353
+ if (approvalId) {
354
+ return await waitForApproval(approvalId, params, progressToken);
355
+ }
356
+ }
330
357
  if (!response.ok)
331
358
  return errorContent(`Proxy error ${response.status}: ${body}`);
332
359
  return { content: [{ type: "text", text: body }] };
333
360
  }
361
+ async function waitForApproval(approvalId, originalCall, progressToken) {
362
+ const startedAt = Date.now();
363
+ let lastProgressAt = 0;
364
+ // Initial progress so the user sees activity immediately.
365
+ await sendProgress(progressToken, 0, APPROVAL_POLL_BUDGET_MS, `Owner approval required for ${originalCall.platform}:${originalCall.scope} — waiting…`);
366
+ while (Date.now() - startedAt < APPROVAL_POLL_BUDGET_MS) {
367
+ await sleep(APPROVAL_POLL_INTERVAL_MS);
368
+ const elapsed = Date.now() - startedAt;
369
+ let res;
370
+ try {
371
+ res = await fetchWithAuth(`${PROXY_URL}/v1/approvals/${approvalId}`, { method: "GET" });
372
+ }
373
+ catch {
374
+ // Transient network error — try again next tick.
375
+ continue;
376
+ }
377
+ if (!res.ok) {
378
+ // 404/410 etc. — treat as terminal. Surface back to the agent.
379
+ const text = await res.text();
380
+ return errorContent(`Approval status error ${res.status}: ${text}`);
381
+ }
382
+ const status = (await res.json());
383
+ if (status.status === "approved" && status.executed_at && status.result) {
384
+ // Re-execution complete. Return the upstream response transparently —
385
+ // the agent sees this as if the original use_platform call just succeeded.
386
+ const r = status.result;
387
+ const summary = `[Owner approved after ${Math.round(elapsed / 1000)}s]\n` +
388
+ JSON.stringify(r.data ?? null);
389
+ // Mirror the non-2xx-from-upstream behaviour of the regular path.
390
+ if (r.status < 200 || r.status >= 300) {
391
+ return errorContent(`Upstream returned ${r.status}: ${JSON.stringify(r.data)}`);
392
+ }
393
+ return { content: [{ type: "text", text: summary }] };
394
+ }
395
+ if (status.status === "denied") {
396
+ return errorContent(`Owner denied this action${status.execution_error ? `: ${status.execution_error}` : "."}`);
397
+ }
398
+ if (status.status === "expired") {
399
+ return errorContent("Approval request expired before the owner responded.");
400
+ }
401
+ if (status.execution_error) {
402
+ // Approved but re-execution failed (network / upstream / agent revoked).
403
+ return errorContent(`Approved but re-execution failed: ${status.execution_error}`);
404
+ }
405
+ // Still pending — emit a progress update at most once per ~5s so the
406
+ // user can see the wait advancing in the Claude Desktop tool UI.
407
+ if (Date.now() - lastProgressAt >= APPROVAL_PROGRESS_INTERVAL_MS) {
408
+ await sendProgress(progressToken, elapsed, APPROVAL_POLL_BUDGET_MS, `Waiting for owner approval — ${Math.round(elapsed / 1000)}s elapsed`);
409
+ lastProgressAt = Date.now();
410
+ }
411
+ }
412
+ // Timed out — fall through to async-recap path. The action is still
413
+ // queued and will run when the owner approves; the user is notified via
414
+ // push/email at that point. Layer 4's list_my_pending_actions surfaces it.
415
+ return {
416
+ content: [{
417
+ type: "text",
418
+ text: JSON.stringify({
419
+ status: "pending_approval",
420
+ approval_id: approvalId,
421
+ message: `Owner hasn't approved within ${APPROVAL_POLL_BUDGET_MS / 1000}s. ` +
422
+ `The action is queued — your owner will be notified, and you'll see it ` +
423
+ `complete next time we chat (or you can ask me to check pending actions).`,
424
+ }),
425
+ }],
426
+ };
427
+ }
428
+ function safeJsonParse(text) {
429
+ try {
430
+ return JSON.parse(text);
431
+ }
432
+ catch {
433
+ return null;
434
+ }
435
+ }
436
+ function sleep(ms) {
437
+ return new Promise((resolve) => setTimeout(resolve, ms));
438
+ }
439
+ async function sendProgress(token, progress, total, message) {
440
+ if (token === undefined)
441
+ return;
442
+ try {
443
+ await server.notification({
444
+ method: "notifications/progress",
445
+ params: { progressToken: token, progress, total, message },
446
+ });
447
+ }
448
+ catch {
449
+ // Best-effort — never let a failed notification break the call.
450
+ }
451
+ }
334
452
  async function handleAgentRegister(args) {
335
453
  let response;
336
454
  try {
@@ -385,6 +503,23 @@ async function handleAuthzenEvaluate(platformId, scope) {
385
503
  return errorContent(`Proxy error ${response.status}: ${body}`);
386
504
  return { content: [{ type: "text", text: body }] };
387
505
  }
506
+ async function handleListMyPendingActions() {
507
+ if (AGENT_PRIVATE_KEY_RAW === null) {
508
+ await notifyBindSecret();
509
+ return pendingFirstCallResponse();
510
+ }
511
+ let response;
512
+ try {
513
+ response = await fetchWithAuth(`${PROXY_URL}/v1/agents/me/pending-actions`, { method: "GET" });
514
+ }
515
+ catch (err) {
516
+ return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
517
+ }
518
+ const text = await response.text();
519
+ if (!response.ok)
520
+ return errorContent(`Proxy error ${response.status}: ${text}`);
521
+ return { content: [{ type: "text", text }] };
522
+ }
388
523
  async function handleReportSelfDiagnostic(args) {
389
524
  if (AGENT_PRIVATE_KEY_RAW === null) {
390
525
  await notifyBindSecret();
@@ -11,8 +11,12 @@ 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
 
18
- Read scopes are auto-approved. Write scopes may require approval. Destructive scopes always require approval.`;
19
+ Read scopes are auto-approved. Write scopes may require approval. Destructive scopes always require approval.
20
+
21
+ Catching up on async actions:
22
+ - If the user mentions a previous action ("did the Slack post go through?", "what happened to that earlier request?") OR if the previous \`use_platform\` returned a \`pending_approval\` envelope, call \`list_my_pending_actions\` to surface what's pending and what's recently completed. Tell the user the result naturally — don't dump the JSON.`;
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.1",
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",