Skip to content

Commit eb31dde

Browse files
committed
Add scoped vault operation and observation hints
1 parent 2d66c94 commit eb31dde

6 files changed

Lines changed: 358 additions & 15 deletions

File tree

docs/vault-payments.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,21 @@ A reusable card remaining `ready` does not establish that the last payment succe
184184

185185
## Observation, updates, and safety
186186

187-
- Single-item responses are JSON text containing `{item, guidance}`. They preserve
187+
- Single-item responses are JSON text containing `{item, hints, guidance}`. They preserve
188188
public state, non-secret aliases, masks, safe action/approval URLs, advertised
189189
operations/expansions, and payment outcomes. Unknown provider fields, opaque
190190
event data, free-form metadata, and URLs carrying OAuth codes/tokens are omitted.
191191
API errors retain the HTTP status but use curated messages for recognized error
192192
codes. Unknown codes use a generic fallback; upstream error text is never returned.
193193
There is no raw-output or raw-card tool.
194+
- `hints.observation` contains `{tool, arguments}` entries for non-blocking `get`
195+
and `events` calls. `hints.invocation` contains only currently advertised
196+
operations, each with `requires_user_approval: true`. Hints preserve the resolved
197+
project selector (when present), vault, and item key. Pass `tool` as the MCP
198+
call's `name` and `arguments` unchanged. Provider-hosted actions remain separate
199+
in `item.action` and approval URLs; they are not callable operation hints.
200+
**A hint is not user approval or a recommendation to retry a payment.**
201+
Availability can change; `invoke` still fetches the item and rechecks it.
194202
- Vault lists return `{items, has_more, next_offset}`. Item lists return `{items}`.
195203
`get` with `expand: ["payment_methods"]` is equivalent to the wallet
196204
`payment_methods` action. An unavailable expansion returns an API error.
@@ -199,9 +207,11 @@ A reusable card remaining `ready` does not establish that the last payment succe
199207
not a background polling loop or readiness guarantee. The SDK timeout is the
200208
wait plus 30 seconds; configure the MCP client's timeout accordingly, or use
201209
shorter waits. Request cancellation is propagated to the SDK.
202-
- `events` accepts `after` and returns `{events, next_after, guidance}`. Pass
203-
`next_after` on the next call for the same vault/key. An empty result preserves
204-
the input cursor (or returns `null` without one).
210+
- `events` accepts `after` and returns `{events, next_after, hints, guidance}`.
211+
Its observation hints include the next events cursor, preserving the input
212+
cursor on an empty result (or omitting `after` when there is no cursor).
213+
Event responses do not include invocation hints because they do not establish
214+
current operation availability.
205215
- **Ready does not mean paid.** Inspect state and immutable events for outcomes.
206216
No vault request is automatically retried. After a failed, timed-out, rejected,
207217
or indeterminate payment, inspect state/events; do not replay checkout, invoke

src/lib/mcp/tools/vault-cards.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ export function registerVaultCardTools(
3838
},
3939
async (params, extra) => {
4040
if (!extra.authInfo) throw new Error("Authentication required");
41+
const project = projectForOperation(extra.authInfo, params);
42+
const target = { project, vault: params.vault, key: params.key };
4143
const client = dependencies.createKernelClient(
4244
extra.authInfo.token,
43-
projectForOperation(extra.authInfo, params),
45+
project,
4446
);
4547
const options = { maxRetries: 0, signal: extra.signal };
4648
try {
@@ -66,7 +68,7 @@ export function registerVaultCardTools(
6668
{ id_or_name: params.vault, spec },
6769
options,
6870
);
69-
return vaultItemResponse(item);
71+
return vaultItemResponse(item, target);
7072
} catch (error) {
7173
throwVaultError("manage_vault_cards", params.action, error);
7274
}

src/lib/mcp/tools/vault-items.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
vaultEventFields,
1212
vaultItemFields,
1313
vaultItemResponse,
14+
vaultObservationHints,
1415
} from "@/lib/mcp/vault-responses";
1516
import {
1617
vaultItemSchema,
@@ -63,9 +64,10 @@ export function registerVaultItemTools(
6364
},
6465
async (params, extra) => {
6566
if (!extra.authInfo) throw new Error("Authentication required");
67+
const project = projectForOperation(extra.authInfo, params);
6668
const client = dependencies.createKernelClient(
6769
extra.authInfo.token,
68-
projectForOperation(extra.authInfo, params),
70+
project,
6971
);
7072
const options = { maxRetries: 0, signal: extra.signal };
7173
try {
@@ -86,6 +88,7 @@ export function registerVaultItemTools(
8688
}
8789
if (!params.key)
8890
return errorResponse("key is required except for list.");
91+
const target = { project, vault: params.vault, key: params.key };
8992
switch (params.action) {
9093
case "get": {
9194
const item = await client.vaults.items.retrieve(
@@ -100,7 +103,7 @@ export function registerVaultItemTools(
100103
signal: extra.signal,
101104
},
102105
);
103-
return vaultItemResponse(item);
106+
return vaultItemResponse(item, target);
104107
}
105108
case "invoke": {
106109
if (!params.operation)
@@ -125,7 +128,7 @@ export function registerVaultItemTools(
125128
},
126129
options,
127130
);
128-
return vaultItemResponse(updated);
131+
return vaultItemResponse(updated, target);
129132
}
130133
case "events": {
131134
const events = await client.vaults.items.events(
@@ -144,9 +147,11 @@ export function registerVaultItemTools(
144147
if (lastEventID !== undefined && typeof lastEventID !== "string") {
145148
throw new Error("Invalid vault event cursor");
146149
}
150+
const nextAfter = lastEventID ?? params.after;
147151
return jsonResponse({
148152
events: projectVaultOutput(events, vaultEventFields),
149-
next_after: lastEventID ?? params.after ?? null,
153+
next_after: nextAfter ?? null,
154+
hints: { observation: vaultObservationHints(target, nextAfter) },
150155
guidance:
151156
"Observing events never retries a payment. Do not retry failed, timed-out, rejected, or indeterminate payments.",
152157
});

src/lib/mcp/tools/vault-wallets.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,11 @@ export function registerVaultWalletTools(
4343
},
4444
async (params, extra) => {
4545
if (!extra.authInfo) throw new Error("Authentication required");
46+
const project = projectForOperation(extra.authInfo, params);
47+
const target = { project, vault: params.vault, key: params.key };
4648
const client = dependencies.createKernelClient(
4749
extra.authInfo.token,
48-
projectForOperation(extra.authInfo, params),
50+
project,
4951
);
5052
const options = { maxRetries: 0, signal: extra.signal };
5153
try {
@@ -74,7 +76,7 @@ export function registerVaultWalletTools(
7476
},
7577
options,
7678
);
77-
return vaultItemResponse(item);
79+
return vaultItemResponse(item, target);
7880
}
7981
case "payment_methods": {
8082
const item = await client.vaults.items.retrieve(
@@ -85,7 +87,7 @@ export function registerVaultWalletTools(
8587
},
8688
{ ...longOperationOptions(0), signal: extra.signal },
8789
);
88-
return vaultItemResponse(item);
90+
return vaultItemResponse(item, target);
8991
}
9092
}
9193
} catch (error) {

0 commit comments

Comments
 (0)