@oobe-protocol-labs/sap-mcp-server 0.9.13 → 0.9.16

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +2 -2
  3. package/USER_DOCS/00_START_HERE.md +24 -2
  4. package/USER_DOCS/03_PAYMENTS_X402_PAYSH.md +6 -1
  5. package/USER_DOCS/04_CLIENT_CONFIGS.md +4 -3
  6. package/USER_DOCS/05_SKILLS_AND_TOOLS.md +9 -4
  7. package/USER_DOCS/06_DESKTOP_GUI_WIZARD.md +6 -6
  8. package/dist/config/mcp-client-injection.d.ts.map +1 -1
  9. package/dist/config/mcp-client-injection.js +4 -3
  10. package/dist/config/mcp-client-injection.js.map +1 -1
  11. package/dist/core/constants.d.ts +9 -1
  12. package/dist/core/constants.d.ts.map +1 -1
  13. package/dist/core/constants.js +13 -3
  14. package/dist/core/constants.js.map +1 -1
  15. package/dist/payments/hosted-tool-eligibility.d.ts.map +1 -1
  16. package/dist/payments/hosted-tool-eligibility.js +3 -0
  17. package/dist/payments/hosted-tool-eligibility.js.map +1 -1
  18. package/dist/payments/index.d.ts +2 -2
  19. package/dist/payments/index.d.ts.map +1 -1
  20. package/dist/payments/index.js +1 -1
  21. package/dist/payments/index.js.map +1 -1
  22. package/dist/payments/pricing.d.ts +30 -0
  23. package/dist/payments/pricing.d.ts.map +1 -1
  24. package/dist/payments/pricing.js +142 -1
  25. package/dist/payments/pricing.js.map +1 -1
  26. package/dist/prompts/context/sap-agent-context.prompt.js +2 -0
  27. package/dist/prompts/context/sap-agent-context.prompt.js.map +1 -1
  28. package/dist/prompts/context/sap-agent-start.prompt.js +2 -0
  29. package/dist/prompts/context/sap-agent-start.prompt.js.map +1 -1
  30. package/dist/prompts/registry/register-sap-agent.prompt.js +18 -18
  31. package/dist/prompts/registry/register-sap-agent.prompt.js.map +1 -1
  32. package/dist/remote/public-home/sections.d.ts.map +1 -1
  33. package/dist/remote/public-home/sections.js +4 -0
  34. package/dist/remote/public-home/sections.js.map +1 -1
  35. package/dist/remote/public-home/types.d.ts +1 -0
  36. package/dist/remote/public-home/types.d.ts.map +1 -1
  37. package/dist/remote/server.d.ts +1 -0
  38. package/dist/remote/server.d.ts.map +1 -1
  39. package/dist/remote/server.js +10 -2
  40. package/dist/remote/server.js.map +1 -1
  41. package/dist/security/tool-permissions.d.ts.map +1 -1
  42. package/dist/security/tool-permissions.js +9 -0
  43. package/dist/security/tool-permissions.js.map +1 -1
  44. package/dist/tools/agent-start-tool.d.ts.map +1 -1
  45. package/dist/tools/agent-start-tool.js +411 -1
  46. package/dist/tools/agent-start-tool.js.map +1 -1
  47. package/dist/tools/client-sdk-tools.js +3 -3
  48. package/dist/tools/client-sdk-tools.js.map +1 -1
  49. package/dist/tools/magicblock-tools.d.ts.map +1 -1
  50. package/dist/tools/magicblock-tools.js +43 -26
  51. package/dist/tools/magicblock-tools.js.map +1 -1
  52. package/dist/tools/sap-sdk-tools.d.ts +412 -1
  53. package/dist/tools/sap-sdk-tools.d.ts.map +1 -1
  54. package/dist/tools/sap-sdk-tools.js +1222 -40
  55. package/dist/tools/sap-sdk-tools.js.map +1 -1
  56. package/dist/tools/sap-sns-tools.d.ts.map +1 -1
  57. package/dist/tools/sap-sns-tools.js +6 -2
  58. package/dist/tools/sap-sns-tools.js.map +1 -1
  59. package/dist/tools/skills-tools.d.ts.map +1 -1
  60. package/dist/tools/skills-tools.js +2 -0
  61. package/dist/tools/skills-tools.js.map +1 -1
  62. package/dist/tools/tool-aliases.d.ts.map +1 -1
  63. package/dist/tools/tool-aliases.js +7 -0
  64. package/dist/tools/tool-aliases.js.map +1 -1
  65. package/dist/tools/x402-paid-call-tool.d.ts.map +1 -1
  66. package/dist/tools/x402-paid-call-tool.js +315 -15
  67. package/dist/tools/x402-paid-call-tool.js.map +1 -1
  68. package/docs/03_CONFIGURATION_AND_WIZARD.md +1 -0
  69. package/docs/06_PAYMENTS_X402_AND_PAYSH.md +56 -3
  70. package/docs/07_ENDPOINTS_AND_CLIENTS.md +2 -1
  71. package/docs/09_TOOLS_SKILLS_AND_AGENT_GUIDE.md +16 -5
  72. package/docs/14_DESKTOP_WIZARD_RELEASE.md +5 -4
  73. package/docs/16_SAP_AGENT_IDENTITY_PIPELINE.md +298 -0
  74. package/docs/README.md +2 -0
  75. package/docs/magicblock-tools.md +26 -0
  76. package/package.json +4 -1
  77. package/server.json +3 -3
  78. package/skills/README.md +10 -2
  79. package/skills/sap-agent-registry/SKILL.md +81 -8
  80. package/skills/sap-discovery-indexing/SKILL.md +11 -0
  81. package/skills/sap-escrow-settlement/SKILL.md +22 -4
  82. package/skills/sap-mcp/SKILL.md +84 -23
  83. package/skills/sap-mcp/TOOL_REFERENCE.md +38 -7
  84. package/skills/sap-nft-metaplex/SKILL.md +22 -0
  85. package/skills/sap-operations/SKILL.md +28 -0
  86. package/skills/sap-payments-x402/SKILL.md +38 -3
  87. package/skills/sap-sns/SKILL.md +17 -0
@@ -5,18 +5,232 @@
5
5
  * `@oobe-protocol-labs/synapse-sap-sdk@1.0.x`. It does not create local
6
6
  * facades for missing SDK namespaces and it does not fabricate network data.
7
7
  */
8
- import { PublicKey } from '@solana/web3.js';
8
+ import { PublicKey, SystemProgram, Transaction } from '@solana/web3.js';
9
9
  import BN from 'bn.js';
10
- import { TOOL_CATEGORY_VALUES } from '@oobe-protocol-labs/synapse-sap-sdk';
10
+ import { Pda, TOOL_CATEGORY_VALUES, USDC_MINT_DEVNET, USDC_MINT_MAINNET } from '@oobe-protocol-labs/synapse-sap-sdk';
11
11
  import { SettlementMode, TokenType, } from '@oobe-protocol-labs/synapse-sap-sdk/types';
12
12
  import { createTextResponse } from '../adapters/mcp/tool-response.js';
13
13
  import { registerTool } from '../adapters/mcp/sdk-compat.js';
14
14
  import { getSapClient, isSapClientInitialized } from '../sap/sap-client-manager.js';
15
15
  import { logger } from '../core/logger.js';
16
+ import { DEFAULT_SAP_PROGRAM_ID, SAP_PROTOCOL_TREASURY, SAP_REGISTRATION_FEE_LAMPORTS, } from '../core/constants.js';
16
17
  const ESCROW_AMOUNT_DESCRIPTION = 'Amount in the escrow token smallest unit: lamports for SOL, micro-USDC for USDC, or base units for the configured SPL token.';
17
18
  const ESCROW_V2_COSIGNED_MODE = 1;
18
19
  const ESCROW_V2_DISPUTE_WINDOW_MODE = 2;
19
20
  const DEFAULT_ESCROW_V2_DISPUTE_WINDOW_SLOTS = new BN(2160);
21
+ const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
22
+ const ASSOCIATED_TOKEN_PROGRAM_ID = new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL');
23
+ export const SAP_AGENT_CAPABILITY_INPUT_SCHEMA = {
24
+ oneOf: [
25
+ {
26
+ type: 'string',
27
+ description: 'Capability id shorthand, for example jupiter:swap, pyth:price, metaplex:identity, sns:identity, or risk:management.',
28
+ },
29
+ {
30
+ type: 'object',
31
+ properties: {
32
+ id: {
33
+ type: 'string',
34
+ description: 'Required stable capability id. Use protocol:action naming, for example jupiter:swap, pyth:price, metaplex:identity, sns:identity, x402:payments, or risk:management.',
35
+ },
36
+ description: {
37
+ type: 'string',
38
+ description: 'Optional human-readable capability description for agents and explorers.',
39
+ },
40
+ protocolId: {
41
+ type: 'string',
42
+ description: 'Optional protocol namespace backing this capability, for example sap, mcp, jupiter, pyth, metaplex, sns, x402, or custom.',
43
+ },
44
+ version: {
45
+ type: 'string',
46
+ description: 'Optional capability version string. Use semver when the capability maps to an API contract.',
47
+ },
48
+ },
49
+ required: ['id'],
50
+ additionalProperties: false,
51
+ },
52
+ ],
53
+ };
54
+ export const SAP_AGENT_PRICING_TIER_INPUT_SCHEMA = {
55
+ type: 'object',
56
+ properties: {
57
+ tierId: {
58
+ type: 'string',
59
+ description: 'Stable tier id, for example default, read, premium, value-action, or enterprise.',
60
+ },
61
+ pricePerCall: {
62
+ type: 'string',
63
+ description: 'Price per call in the smallest unit of tokenType: lamports for sol, micro-USDC for usdc, or base units for spl.',
64
+ },
65
+ minPricePerCall: {
66
+ type: 'string',
67
+ description: 'Optional floor price per call in the smallest unit of tokenType.',
68
+ },
69
+ maxPricePerCall: {
70
+ type: 'string',
71
+ description: 'Optional ceiling price per call in the smallest unit of tokenType.',
72
+ },
73
+ rateLimit: {
74
+ type: 'number',
75
+ description: 'Maximum calls per second allowed by this tier. Defaults to 60.',
76
+ },
77
+ maxCallsPerSession: {
78
+ type: 'number',
79
+ description: 'Maximum calls allowed per session for this tier. Defaults to 1000.',
80
+ },
81
+ burstLimit: {
82
+ type: 'number',
83
+ description: 'Optional short-window burst allowance for this tier.',
84
+ },
85
+ tokenType: {
86
+ type: 'string',
87
+ enum: ['sol', 'usdc', 'spl'],
88
+ description: 'Payment token type. Use usdc for x402/pay.sh agent commerce. Use spl only with tokenMint.',
89
+ },
90
+ tokenMint: {
91
+ type: 'string',
92
+ description: 'Optional SPL token mint public key. Required when tokenType is spl. For mainnet USDC use EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v.',
93
+ },
94
+ tokenDecimals: {
95
+ type: 'number',
96
+ description: 'Decimals for the payment token. Defaults to 6 for usdc and 9 for sol when omitted.',
97
+ },
98
+ settlementMode: {
99
+ type: 'string',
100
+ enum: ['instant', 'escrow', 'batched', 'x402'],
101
+ description: 'Settlement strategy. Use x402 for HTTP 402/pay.sh flows, escrow for prepaid SAP usage ledgers, instant for direct pay-per-call, and batched for periodic settlement.',
102
+ },
103
+ minEscrowDeposit: {
104
+ type: 'string',
105
+ description: 'Optional minimum escrow deposit in the smallest unit of tokenType.',
106
+ },
107
+ batchIntervalSec: {
108
+ type: 'number',
109
+ description: 'Optional settlement batch interval in seconds when settlementMode is batched.',
110
+ },
111
+ volumeCurve: {
112
+ type: 'array',
113
+ description: 'Optional volume discounts. Each item lowers pricePerCall after a call threshold.',
114
+ items: {
115
+ type: 'object',
116
+ properties: {
117
+ afterCalls: { type: 'number', description: 'Call count threshold where this price starts.' },
118
+ pricePerCall: { type: 'string', description: 'Discounted price per call in the smallest unit of tokenType.' },
119
+ },
120
+ required: ['afterCalls', 'pricePerCall'],
121
+ additionalProperties: false,
122
+ },
123
+ },
124
+ },
125
+ required: ['pricePerCall'],
126
+ additionalProperties: false,
127
+ };
128
+ export const SAP_AGENT_REGISTER_INPUT_SCHEMA = {
129
+ name: {
130
+ type: 'string',
131
+ description: 'Required public display name for the SAP agent. Keep it stable enough for explorers and humans.',
132
+ },
133
+ description: {
134
+ type: 'string',
135
+ description: 'Required public description of what the agent does, which protocols it can use, and its safety/trust boundaries.',
136
+ },
137
+ capabilities: {
138
+ type: 'array',
139
+ description: 'Required capability list. Prefer object form for production; strings are accepted as shorthand.',
140
+ items: SAP_AGENT_CAPABILITY_INPUT_SCHEMA,
141
+ },
142
+ pricing: {
143
+ type: 'array',
144
+ description: 'Optional pricing tiers advertised by the agent. Use tokenType usdc + settlementMode x402 for pay.sh/x402 agent commerce.',
145
+ items: SAP_AGENT_PRICING_TIER_INPUT_SCHEMA,
146
+ },
147
+ protocols: {
148
+ type: 'array',
149
+ items: { type: 'string' },
150
+ description: 'Required protocol tags the agent supports, for example sap, mcp, jupiter, pyth, metaplex, sns, x402, payments.',
151
+ },
152
+ agentId: {
153
+ type: 'string',
154
+ description: 'Optional stable lowercase agent id, for example solking. This is separate from the on-chain agent PDA.',
155
+ },
156
+ agentUri: {
157
+ type: 'string',
158
+ description: 'Optional public HTTPS/IPFS/Arweave/Kommodo URI for the agent profile metadata or profile page. Never use local desktop file paths.',
159
+ },
160
+ metadataUri: {
161
+ type: 'string',
162
+ description: 'Alias for agentUri. Prefer a public JSON metadata document containing name, description, image, external_url, attributes, sap, metaplex, sns, and x402 fields.',
163
+ },
164
+ x402Endpoint: {
165
+ type: 'string',
166
+ description: 'Optional public x402 discovery/payment endpoint, usually https://host/.well-known/x402 for external agent services.',
167
+ },
168
+ };
169
+ export const SAP_AGENT_UPDATE_INPUT_SCHEMA = {
170
+ name: {
171
+ type: 'string',
172
+ description: 'Optional replacement display name. Omit to keep the current name.',
173
+ },
174
+ description: {
175
+ type: 'string',
176
+ description: 'Optional replacement description. Omit to keep the current description.',
177
+ },
178
+ capabilities: {
179
+ type: 'array',
180
+ description: 'Optional full replacement capability list. Omit to keep current capabilities; do not send only the new item unless replacing the whole list is intended.',
181
+ items: SAP_AGENT_CAPABILITY_INPUT_SCHEMA,
182
+ },
183
+ pricing: {
184
+ type: 'array',
185
+ description: 'Optional full replacement pricing tier list. Omit to keep current pricing.',
186
+ items: SAP_AGENT_PRICING_TIER_INPUT_SCHEMA,
187
+ },
188
+ protocols: {
189
+ type: 'array',
190
+ items: { type: 'string' },
191
+ description: 'Optional full replacement protocol list. Omit to keep current protocols.',
192
+ },
193
+ agentId: {
194
+ type: 'string',
195
+ description: 'Optional replacement stable agent id. Omit to keep current agentId.',
196
+ },
197
+ agentUri: {
198
+ type: 'string',
199
+ description: 'Optional replacement public URI for agent metadata/profile. Use this to update pictures after uploading metadata to IPFS, Arweave, Kommodo, or HTTPS.',
200
+ },
201
+ metadataUri: {
202
+ type: 'string',
203
+ description: 'Alias for agentUri. Use a public metadata JSON URI; never use a desktop file path.',
204
+ },
205
+ x402Endpoint: {
206
+ type: 'string',
207
+ description: 'Optional replacement x402 discovery/payment endpoint. Omit to keep current endpoint.',
208
+ },
209
+ };
210
+ const SAP_AGENT_IDENTITY_PLAN_INPUT_SCHEMA = {
211
+ intendedAction: {
212
+ type: 'string',
213
+ enum: ['register', 'update', 'link-metaplex', 'link-sns', 'full-identity'],
214
+ description: 'Agent identity lifecycle step to plan. Use register for a new SAP agent, update for profile/image/capability changes, link-metaplex for NFT/MPL Core identity, link-sns for .sol identity, or full-identity for the recommended end-to-end flow.',
215
+ },
216
+ ...SAP_AGENT_REGISTER_INPUT_SCHEMA,
217
+ snsDomain: {
218
+ type: 'string',
219
+ description: 'Optional .sol domain to link after ownership or registration is verified.',
220
+ },
221
+ imageUrl: {
222
+ type: 'string',
223
+ description: 'Optional public image URL. This must be HTTPS, IPFS, Arweave, or another public URI; desktop file paths are not valid.',
224
+ },
225
+ metaplexAsset: {
226
+ type: 'string',
227
+ description: 'Optional Metaplex NFT or MPL Core asset/collection address to reference from public agent metadata.',
228
+ },
229
+ ownerWallet: {
230
+ type: 'string',
231
+ description: 'Optional expected SAP owner wallet. Use this for verification planning only; signing still comes from the active local SAP profile.',
232
+ },
233
+ };
20
234
  const escrowV2CreateInputSchema = {
21
235
  agentWallet: {
22
236
  type: 'string',
@@ -68,6 +282,124 @@ const escrowV2CreateInputSchema = {
68
282
  description: 'Optional arbiter public key kept for IDL compatibility and dispute workflows.',
69
283
  },
70
284
  };
285
+ const escrowV2CreateBuilderInputSchema = {
286
+ depositorWallet: {
287
+ type: 'string',
288
+ description: 'Depositor wallet public key (base58). This wallet signs locally and funds the escrow; hosted SAP MCP never receives its private key.',
289
+ },
290
+ ...escrowV2CreateInputSchema,
291
+ };
292
+ const escrowV2DepositBuilderInputSchema = {
293
+ depositorWallet: {
294
+ type: 'string',
295
+ description: 'Depositor wallet public key (base58). This wallet signs locally and provides the additional escrow funds.',
296
+ },
297
+ agentWallet: {
298
+ type: 'string',
299
+ description: 'Agent owner wallet public key (base58). The escrow PDA is derived from agentWallet, depositorWallet, and nonce.',
300
+ },
301
+ nonce: {
302
+ type: 'string',
303
+ description: 'Optional escrow nonce as a decimal string. Defaults to 0.',
304
+ },
305
+ amount: {
306
+ type: 'string',
307
+ description: `Deposit amount as a decimal string. ${ESCROW_AMOUNT_DESCRIPTION}`,
308
+ },
309
+ };
310
+ const escrowV2WithdrawBuilderInputSchema = {
311
+ depositorWallet: {
312
+ type: 'string',
313
+ description: 'Depositor wallet public key (base58). Only the depositor can locally sign a withdrawal from this escrow.',
314
+ },
315
+ agentWallet: {
316
+ type: 'string',
317
+ description: 'Agent owner wallet public key (base58). The escrow PDA is derived from agentWallet, depositorWallet, and nonce.',
318
+ },
319
+ nonce: {
320
+ type: 'string',
321
+ description: 'Optional escrow nonce as a decimal string. Defaults to 0.',
322
+ },
323
+ amount: {
324
+ type: 'string',
325
+ description: `Withdrawal amount as a decimal string. ${ESCROW_AMOUNT_DESCRIPTION}`,
326
+ },
327
+ };
328
+ const escrowV2SettleBuilderInputSchema = {
329
+ agentWallet: {
330
+ type: 'string',
331
+ description: 'Agent owner wallet public key (base58). This wallet signs locally because settlement releases funds to the serving agent.',
332
+ },
333
+ depositorWallet: {
334
+ type: 'string',
335
+ description: 'Depositor wallet public key (base58) for the escrow being settled.',
336
+ },
337
+ nonce: {
338
+ type: 'string',
339
+ description: 'Optional escrow nonce as a decimal string. Defaults to 0.',
340
+ },
341
+ callsToSettle: {
342
+ type: 'string',
343
+ description: 'Number of served calls to settle as a decimal string.',
344
+ },
345
+ serviceHash: {
346
+ oneOf: [
347
+ {
348
+ type: 'array',
349
+ items: { type: 'number' },
350
+ description: '32-byte service/audit hash as byte array.',
351
+ },
352
+ {
353
+ type: 'string',
354
+ description: '32-byte service/audit hash encoded as 64-char hex or base64 string.',
355
+ },
356
+ ],
357
+ description: '32-byte service/audit hash. Use a stable hash of the fulfilled paid work.',
358
+ },
359
+ coSigner: {
360
+ type: 'string',
361
+ description: 'Optional co-signer public key for CoSigned escrows when the escrow account requires it.',
362
+ },
363
+ };
364
+ const escrowV2FinalizeBuilderInputSchema = {
365
+ payerWallet: {
366
+ type: 'string',
367
+ description: 'Wallet public key (base58) that signs and pays transaction fees to finalize the pending settlement.',
368
+ },
369
+ agentWallet: {
370
+ type: 'string',
371
+ description: 'Agent owner wallet public key (base58).',
372
+ },
373
+ depositorWallet: {
374
+ type: 'string',
375
+ description: 'Depositor wallet public key (base58).',
376
+ },
377
+ nonce: {
378
+ type: 'string',
379
+ description: 'Optional escrow nonce as a decimal string. Defaults to 0.',
380
+ },
381
+ settlementIndex: {
382
+ type: 'string',
383
+ description: 'Pending settlement index as a decimal string.',
384
+ },
385
+ };
386
+ const escrowV2CloseBuilderInputSchema = {
387
+ depositorWallet: {
388
+ type: 'string',
389
+ description: 'Depositor wallet public key (base58). The depositor signs locally to close the empty escrow.',
390
+ },
391
+ agentWallet: {
392
+ type: 'string',
393
+ description: 'Agent owner wallet public key (base58).',
394
+ },
395
+ nonce: {
396
+ type: 'string',
397
+ description: 'Optional escrow nonce as a decimal string. Defaults to 0.',
398
+ },
399
+ };
400
+ const DIRECTORY_CACHE_TTL_MS = 10_000;
401
+ const directoryPageCache = new Map();
402
+ const directoryPageInflight = new Map();
71
403
  /**
72
404
  * @name jsonReplacer
73
405
  * @description Serializes SDK values such as PublicKey, BN, bigint, Buffer, and Uint8Array into JSON-safe output.
@@ -110,6 +442,22 @@ function asObjectPayload(payload) {
110
442
  function getSapAnchorAccounts(client) {
111
443
  return client.program.account;
112
444
  }
445
+ function stableJson(value) {
446
+ if (value === undefined) {
447
+ return 'undefined';
448
+ }
449
+ if (value === null) {
450
+ return 'null';
451
+ }
452
+ if (typeof value !== 'object') {
453
+ return JSON.stringify(value) ?? String(value);
454
+ }
455
+ if (Array.isArray(value)) {
456
+ return `[${value.map(stableJson).join(',')}]`;
457
+ }
458
+ const record = value;
459
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(',')}}`;
460
+ }
113
461
  /**
114
462
  * @name bnString
115
463
  * @description Converts BN-like SDK counters into stable decimal strings.
@@ -362,6 +710,113 @@ async function buildAgentDirectoryPage(client, options) {
362
710
  agentGuidance: directoryAgentGuidance({ hasMore, nextCursor, filters: options }),
363
711
  };
364
712
  }
713
+ async function getCachedAgentDirectoryPage(client, options) {
714
+ const cacheKey = stableJson(options);
715
+ const now = Date.now();
716
+ const cached = directoryPageCache.get(cacheKey);
717
+ if (cached && cached.expiresAt > now) {
718
+ return {
719
+ ...cached.page,
720
+ cache: {
721
+ status: 'hit',
722
+ ttlMs: Math.max(0, cached.expiresAt - now),
723
+ key: cacheKey.slice(0, 24),
724
+ },
725
+ };
726
+ }
727
+ const inflight = directoryPageInflight.get(cacheKey);
728
+ if (inflight) {
729
+ const page = await inflight;
730
+ return {
731
+ ...page,
732
+ cache: {
733
+ status: 'joined_inflight',
734
+ ttlMs: DIRECTORY_CACHE_TTL_MS,
735
+ key: cacheKey.slice(0, 24),
736
+ },
737
+ };
738
+ }
739
+ const load = buildAgentDirectoryPage(client, options)
740
+ .then((page) => {
741
+ directoryPageCache.set(cacheKey, { expiresAt: Date.now() + DIRECTORY_CACHE_TTL_MS, page });
742
+ return page;
743
+ })
744
+ .finally(() => {
745
+ directoryPageInflight.delete(cacheKey);
746
+ });
747
+ directoryPageInflight.set(cacheKey, load);
748
+ const page = await load;
749
+ return {
750
+ ...page,
751
+ cache: {
752
+ status: 'miss',
753
+ ttlMs: DIRECTORY_CACHE_TTL_MS,
754
+ key: cacheKey.slice(0, 24),
755
+ },
756
+ };
757
+ }
758
+ async function buildSapAgentContext(input, client) {
759
+ const wallet = optionalString(input, 'wallet');
760
+ const agentPda = optionalString(input, 'agentPda');
761
+ const query = optionalString(input, 'query');
762
+ const limit = Math.max(1, Math.min(optionalNumber(input, 'limit') ?? 10, 20));
763
+ const exact = {};
764
+ if (wallet) {
765
+ const owner = new PublicKey(wallet);
766
+ const derivedAgentPda = client.agent.deriveAgent(owner)[0].toBase58();
767
+ const [agent, active, profile] = await Promise.all([
768
+ client.agent.fetchNullable(owner),
769
+ client.discovery.isAgentActive(owner).catch(() => false),
770
+ client.discovery.getAgentProfile(owner).catch((error) => ({
771
+ unavailable: true,
772
+ reason: error instanceof Error ? error.message : 'profile_fetch_failed',
773
+ })),
774
+ ]);
775
+ exact.wallet = wallet;
776
+ exact.agentPda = derivedAgentPda;
777
+ exact.active = active;
778
+ exact.agent = agent;
779
+ exact.profile = profile;
780
+ }
781
+ const directory = wallet
782
+ ? null
783
+ : await getCachedAgentDirectoryPage(client, {
784
+ includeInactive: false,
785
+ protocol: undefined,
786
+ capability: undefined,
787
+ capabilities: [],
788
+ capabilityMode: 'any',
789
+ query,
790
+ wallet: undefined,
791
+ agentPda,
792
+ hasX402Endpoint: undefined,
793
+ limit,
794
+ offset: 0,
795
+ view: 'compact',
796
+ includeProtocolIndexes: false,
797
+ });
798
+ return {
799
+ contextType: wallet ? 'exact-wallet' : agentPda ? 'exact-agent-pda' : query ? 'compact-query' : 'compact-orientation',
800
+ freeRead: true,
801
+ exact,
802
+ directory,
803
+ routing: {
804
+ firstReads: [
805
+ 'Use sap_get_agent or sap_get_agent_profile when the owner wallet is known.',
806
+ 'Use sap_list_agents with limit <= 20 and view: compact for free orientation.',
807
+ 'Use sap_discover_agents or sap_list_all_agents only for paid search, enriched rows, large pages, or ecosystem-scale scans.',
808
+ ],
809
+ paidHostedTools: 'Use sap_payments_call_paid_tool when a hosted tool returns x402 payment_required and the runtime cannot replay x402 natively.',
810
+ registryWrites: 'Use sap_payments_register_agent and sap_payments_update_agent for wallet-owned SAP registry writes. Do not retry hosted accountless writes after hosted_local_signer_required.',
811
+ unsignedTransactions: 'Use hosted builders when available, then sap_payments_finalize_transaction with submit:true after user confirmation.',
812
+ },
813
+ nextActions: [
814
+ wallet ? 'If the user wants details, use the exact profile already returned here before paid discovery.' : 'If the user selects an agent row, call sap_get_agent_profile with that row wallet.',
815
+ 'For registration or image/profile updates, call sap_agent_identity_plan before any write.',
816
+ 'For paid or write flows, call sap_payments_readiness first when the local bridge is visible.',
817
+ ],
818
+ };
819
+ }
365
820
  function parseCapabilityMode(input) {
366
821
  const mode = optionalString(input, 'capabilityMode')?.trim().toLowerCase();
367
822
  if (!mode) {
@@ -594,6 +1049,45 @@ function parseVolumeCurve(value) {
594
1049
  };
595
1050
  });
596
1051
  }
1052
+ /**
1053
+ * @name optionalTokenType
1054
+ * @description Converts public tokenType strings into SDK Anchor enum variants.
1055
+ */
1056
+ function optionalTokenType(input) {
1057
+ const raw = optionalString(input, 'tokenType') ?? 'sol';
1058
+ const normalized = raw.trim().toLowerCase();
1059
+ if (normalized === 'sol' || normalized === 'native' || normalized === 'lamports') {
1060
+ return { tokenType: TokenType.Sol, decimalsFallback: 9 };
1061
+ }
1062
+ if (normalized === 'usdc' || normalized === 'micro-usdc' || normalized === 'micro_usdc') {
1063
+ return { tokenType: TokenType.Usdc, decimalsFallback: 6 };
1064
+ }
1065
+ if (normalized === 'spl' || normalized === 'token') {
1066
+ return { tokenType: TokenType.Spl, decimalsFallback: 0 };
1067
+ }
1068
+ throw new Error('tokenType must be one of sol, usdc, or spl');
1069
+ }
1070
+ /**
1071
+ * @name optionalSettlementMode
1072
+ * @description Converts public settlementMode strings into SDK Anchor enum variants.
1073
+ */
1074
+ function optionalSettlementMode(input) {
1075
+ const raw = optionalString(input, 'settlementMode') ?? 'escrow';
1076
+ const normalized = raw.trim().toLowerCase();
1077
+ if (normalized === 'instant') {
1078
+ return SettlementMode.Instant;
1079
+ }
1080
+ if (normalized === 'escrow') {
1081
+ return SettlementMode.Escrow;
1082
+ }
1083
+ if (normalized === 'batched' || normalized === 'batch') {
1084
+ return SettlementMode.Batched;
1085
+ }
1086
+ if (normalized === 'x402' || normalized === 'pay.sh' || normalized === 'paysh') {
1087
+ return SettlementMode.X402;
1088
+ }
1089
+ throw new Error('settlementMode must be one of instant, escrow, batched, or x402');
1090
+ }
597
1091
  /**
598
1092
  * @name requiredBytes
599
1093
  * @description Reads byte-array fields used for hashes, nonces, and encrypted payloads.
@@ -700,6 +1194,12 @@ function parsePricingTiers(value) {
700
1194
  return value.map((item) => {
701
1195
  const record = asRecord(item);
702
1196
  const pricePerCall = optionalBn(record, 'pricePerCall', new BN(0));
1197
+ const { tokenType, decimalsFallback } = optionalTokenType(record);
1198
+ const tokenMint = optionalPublicKey(record, 'tokenMint') ?? null;
1199
+ if (tokenType === TokenType.Spl && tokenMint === null) {
1200
+ throw new Error('pricing.tokenMint is required when tokenType is spl');
1201
+ }
1202
+ const volumeCurve = parseVolumeCurve(record.volumeCurve);
703
1203
  return {
704
1204
  tierId: optionalString(record, 'tierId') ?? 'default',
705
1205
  pricePerCall,
@@ -708,13 +1208,13 @@ function parsePricingTiers(value) {
708
1208
  rateLimit: optionalNumber(record, 'rateLimit') ?? 60,
709
1209
  maxCallsPerSession: optionalNumber(record, 'maxCallsPerSession') ?? 1_000,
710
1210
  burstLimit: optionalNumber(record, 'burstLimit') ?? null,
711
- tokenType: TokenType.Sol,
712
- tokenMint: optionalPublicKey(record, 'tokenMint') ?? null,
713
- tokenDecimals: optionalNumber(record, 'tokenDecimals') ?? null,
714
- settlementMode: SettlementMode.Escrow,
1211
+ tokenType,
1212
+ tokenMint,
1213
+ tokenDecimals: optionalNumber(record, 'tokenDecimals') ?? decimalsFallback,
1214
+ settlementMode: optionalSettlementMode(record),
715
1215
  minEscrowDeposit: record.minEscrowDeposit === undefined ? null : requiredBn(record, 'minEscrowDeposit'),
716
1216
  batchIntervalSec: optionalNumber(record, 'batchIntervalSec') ?? null,
717
- volumeCurve: null,
1217
+ volumeCurve: volumeCurve.length > 0 ? volumeCurve : null,
718
1218
  };
719
1219
  });
720
1220
  }
@@ -756,7 +1256,7 @@ export function parseRegisterAgentArgs(input) {
756
1256
  * @name parseUpdateAgentArgs
757
1257
  * @description Builds strongly typed `UpdateAgentArgs` from MCP JSON input.
758
1258
  */
759
- function parseUpdateAgentArgs(input) {
1259
+ export function parseUpdateAgentArgs(input) {
760
1260
  return {
761
1261
  name: optionalString(input, 'name') ?? null,
762
1262
  description: optionalString(input, 'description') ?? null,
@@ -768,6 +1268,168 @@ function parseUpdateAgentArgs(input) {
768
1268
  x402Endpoint: optionalString(input, 'x402Endpoint') ?? null,
769
1269
  };
770
1270
  }
1271
+ function parseIdentityPlanAction(input) {
1272
+ const value = optionalString(input, 'intendedAction') ?? 'full-identity';
1273
+ if (['register', 'update', 'link-metaplex', 'link-sns', 'full-identity'].includes(value)) {
1274
+ return value;
1275
+ }
1276
+ throw new Error('intendedAction must be one of register, update, link-metaplex, link-sns, full-identity');
1277
+ }
1278
+ function optionalPublicMetadataUri(input) {
1279
+ return optionalString(input, 'metadataUri') ?? optionalString(input, 'agentUri') ?? null;
1280
+ }
1281
+ function buildSapAgentIdentityPlan(input) {
1282
+ const intendedAction = parseIdentityPlanAction(input);
1283
+ const metadataUri = optionalPublicMetadataUri(input);
1284
+ const imageUrl = optionalString(input, 'imageUrl') ?? null;
1285
+ const metaplexAsset = optionalString(input, 'metaplexAsset') ?? null;
1286
+ const snsDomain = optionalString(input, 'snsDomain') ?? null;
1287
+ const ownerWallet = optionalString(input, 'ownerWallet') ?? null;
1288
+ const missingRegisterFields = ['name', 'description', 'capabilities', 'protocols']
1289
+ .filter((field) => input[field] === undefined || input[field] === null);
1290
+ const hasCompleteRegisterFields = missingRegisterFields.length === 0;
1291
+ const registerArgs = hasCompleteRegisterFields
1292
+ ? parseRegisterAgentArgs({
1293
+ ...input,
1294
+ capabilities: input.capabilities ?? [],
1295
+ pricing: input.pricing ?? [],
1296
+ protocols: input.protocols ?? [],
1297
+ })
1298
+ : null;
1299
+ const updateArgs = parseUpdateAgentArgs(input);
1300
+ return {
1301
+ intendedAction,
1302
+ trustBoundary: {
1303
+ hostedSap: 'Use hosted sap for reads, paid hosted tools, and unsigned builders.',
1304
+ localSapPayments: 'Use local sap_payments for x402 payment signing, SAP registry writes, and transaction finalization.',
1305
+ keypairMaterial: 'Never read or export keypair JSON. Only local SAP MCP signer tools may sign.',
1306
+ },
1307
+ nextTools: {
1308
+ readiness: 'sap_payments_readiness',
1309
+ register: intendedAction === 'register' || intendedAction === 'full-identity' ? 'sap_payments_register_agent' : null,
1310
+ update: intendedAction !== 'register' ? 'sap_payments_update_agent' : null,
1311
+ metaplex: metaplexAsset ? 'metaplex-nft_* or DAS verification tools before SAP metadata update' : null,
1312
+ sns: snsDomain ? ['sap_sns_check_domain', 'sap_sns_check_ownership', 'sap_sns_build_manage_record_transaction when managing records'] : null,
1313
+ profileVerification: 'sap_get_agent_profile',
1314
+ },
1315
+ missingRegisterFields,
1316
+ normalizedRegistration: registerArgs
1317
+ ? {
1318
+ name: registerArgs.name,
1319
+ description: registerArgs.description,
1320
+ agentId: registerArgs.agentId,
1321
+ agentUri: registerArgs.agentUri,
1322
+ x402Endpoint: registerArgs.x402Endpoint,
1323
+ protocols: registerArgs.protocols,
1324
+ capabilities: registerArgs.capabilities,
1325
+ pricing: registerArgs.pricing,
1326
+ confirm: true,
1327
+ }
1328
+ : null,
1329
+ normalizedUpdate: {
1330
+ name: updateArgs.name,
1331
+ description: updateArgs.description,
1332
+ agentId: updateArgs.agentId,
1333
+ agentUri: updateArgs.agentUri,
1334
+ x402Endpoint: updateArgs.x402Endpoint,
1335
+ protocols: updateArgs.protocols,
1336
+ capabilities: updateArgs.capabilities,
1337
+ pricing: updateArgs.pricing,
1338
+ confirm: true,
1339
+ },
1340
+ publicMetadataContract: {
1341
+ requiredForImages: true,
1342
+ metadataUri,
1343
+ imageUrl,
1344
+ metaplexAsset,
1345
+ snsDomain,
1346
+ recommendedShape: {
1347
+ name: input.name ?? '<agent name>',
1348
+ description: input.description ?? '<agent description>',
1349
+ image: imageUrl ?? '<public image URL>',
1350
+ external_url: ownerWallet ? `https://explorer.oobeprotocol.ai/agents/${ownerWallet}` : '<public agent page>',
1351
+ attributes: [
1352
+ { trait_type: 'Protocol', value: 'SAP' },
1353
+ { trait_type: 'Identity', value: [metaplexAsset ? 'Metaplex' : null, snsDomain ? 'SNS' : null].filter(Boolean).join(' + ') || 'SAP' },
1354
+ ],
1355
+ sap: {
1356
+ ownerWallet: ownerWallet ?? '<owner wallet>',
1357
+ agentId: optionalString(input, 'agentId') ?? '<stable id>',
1358
+ capabilities: Array.isArray(input.capabilities) ? input.capabilities : [],
1359
+ },
1360
+ metaplex: metaplexAsset ? { asset: metaplexAsset } : null,
1361
+ sns: snsDomain ? { domain: snsDomain } : null,
1362
+ x402: optionalString(input, 'x402Endpoint') ? { endpoint: optionalString(input, 'x402Endpoint') } : null,
1363
+ },
1364
+ },
1365
+ verificationChecklist: [
1366
+ 'Call sap_payments_readiness before paid/write actions.',
1367
+ 'For registration, call sap_payments_register_agent with confirm:true; do not call hosted sap_register_agent after hosted_local_signer_required.',
1368
+ 'Verify sap_payments_register_agent success, confirmationStatus, agentPda, and protocolFee.status.',
1369
+ 'Fetch sap_get_agent_profile by owner wallet after registration or update.',
1370
+ 'For update, verify every intended replacement field because arrays are full replacements.',
1371
+ 'For Metaplex identity, verify the asset/collection metadata URI and update authority before linking it from SAP metadata.',
1372
+ 'For SNS identity, verify domain ownership and records before claiming the .sol identity is linked.',
1373
+ ],
1374
+ forbiddenActions: [
1375
+ 'Do not create temporary signing scripts.',
1376
+ 'Do not read keypair JSON.',
1377
+ 'Do not reuse stale x402 payment signatures.',
1378
+ 'Do not pay hosted x402 fees for hosted_local_signer_required registry writes.',
1379
+ 'Do not announce lifecycle complete if protocolFee.status is missing_or_underpaid.',
1380
+ ],
1381
+ };
1382
+ }
1383
+ function buildSapProtocolInvariants() {
1384
+ return {
1385
+ protocol: {
1386
+ name: 'Synapse Agent Protocol',
1387
+ programId: DEFAULT_SAP_PROGRAM_ID,
1388
+ network: 'mainnet-beta',
1389
+ custodyModel: 'non-custodial',
1390
+ },
1391
+ registrationFee: {
1392
+ sourceExpected: true,
1393
+ treasury: SAP_PROTOCOL_TREASURY,
1394
+ lamports: SAP_REGISTRATION_FEE_LAMPORTS.toString(10),
1395
+ sol: Number(SAP_REGISTRATION_FEE_LAMPORTS) / 1_000_000_000,
1396
+ verification: 'sap_payments_register_agent verifies the landed transaction pre/post balance delta for the treasury account because deployed program behavior can drift from the source-level invariant.',
1397
+ failureStatus: 'missing_or_underpaid',
1398
+ failureRule: 'If protocolFee.status is missing_or_underpaid, the agent account can still exist, but sap_payments_register_agent must return success:false, agentRegistered:true, and protocolComplete:false. Do not retry registration automatically; inspect the signature, deployed SAP program, and treasury delta first.',
1399
+ },
1400
+ hostedWritePolicy: {
1401
+ register: 'Hosted sap_register_agent is accountless and returns hosted_local_signer_required before x402 payment.',
1402
+ update: 'Hosted sap_update_agent is accountless and returns hosted_local_signer_required before x402 payment.',
1403
+ sns: 'Hosted direct SNS writes require a local signer or unsigned builder. If no builder exists, stop and route through local SAP MCP.',
1404
+ noChargeRule: 'hosted_local_signer_required is a routing guard, not a paid failure; no hosted x402 fee should be charged for that blocked write.',
1405
+ },
1406
+ localSignerRoutes: {
1407
+ readiness: 'sap_payments_readiness',
1408
+ registerAgent: 'sap_payments_register_agent',
1409
+ updateAgent: 'sap_payments_update_agent',
1410
+ finalizeUnsignedTransaction: 'sap_payments_finalize_transaction',
1411
+ paidHostedTool: 'sap_payments_call_paid_tool',
1412
+ externalX402Http: 'sap_payments_call_external_x402',
1413
+ },
1414
+ identityPipeline: [
1415
+ 'Call sap_agent_identity_plan for register, update, Metaplex, SNS, or full-identity intent.',
1416
+ 'Upload image and metadata to a public URL before writing agentUri or metadataUri.',
1417
+ 'Use sap_payments_register_agent for local non-custodial registration.',
1418
+ 'Verify success, agentRegistered, confirmationStatus, agentPda, protocolComplete, and protocolFee.status.',
1419
+ 'Use Metaplex/MPL Core tools only after metadata authority and URI are clear.',
1420
+ 'Use SNS tools only after domain availability/ownership and user confirmation are clear.',
1421
+ 'Use sap_payments_update_agent for profile image, metadata, capabilities, pricing, protocols, or x402 endpoint updates.',
1422
+ 'Fetch sap_get_agent_profile after every registry write and compare intended fields.',
1423
+ ],
1424
+ forbiddenActions: [
1425
+ 'Do not create temporary signing scripts.',
1426
+ 'Do not read keypair JSON.',
1427
+ 'Do not call hosted sap_sign_transaction for user-owned signatures.',
1428
+ 'Do not call hosted sap_register_agent or sap_update_agent again after hosted_local_signer_required.',
1429
+ 'Do not call an agent lifecycle complete when protocolFee.status is missing_or_underpaid, unavailable, or anything other than verified.',
1430
+ ],
1431
+ };
1432
+ }
771
1433
  /**
772
1434
  * @name parseX402PreparePaymentOptions
773
1435
  * @description Builds typed x402 payment preparation options from MCP JSON input.
@@ -885,6 +1547,433 @@ function parseEscrowV2Args(input) {
885
1547
  arbiter: optionalPublicKey(input, 'arbiter') ?? null,
886
1548
  };
887
1549
  }
1550
+ function publicKeyOrNull(value) {
1551
+ return value instanceof PublicKey ? value : null;
1552
+ }
1553
+ function bnToBigInt(value) {
1554
+ if (typeof value === 'bigint') {
1555
+ return value;
1556
+ }
1557
+ if (typeof value === 'number') {
1558
+ return BigInt(value);
1559
+ }
1560
+ return BigInt(value.toString());
1561
+ }
1562
+ function bnToPdaSeed(value, field) {
1563
+ const asBigInt = BigInt(value.toString(10));
1564
+ if (asBigInt < 0n) {
1565
+ throw new Error(`${field} must be >= 0`);
1566
+ }
1567
+ return asBigInt;
1568
+ }
1569
+ function serializeAccountMetas(accounts) {
1570
+ return accounts.map((account) => ({
1571
+ pubkey: account.pubkey.toBase58(),
1572
+ isSigner: account.isSigner,
1573
+ isWritable: account.isWritable,
1574
+ }));
1575
+ }
1576
+ function validateEscrowPaymentMint(tokenMint) {
1577
+ if (!tokenMint) {
1578
+ return;
1579
+ }
1580
+ if (!tokenMint.equals(USDC_MINT_MAINNET) && !tokenMint.equals(USDC_MINT_DEVNET)) {
1581
+ throw new Error('Escrow V2 hosted builders only accept native SOL or USDC mints supported by the current SAP program.');
1582
+ }
1583
+ }
1584
+ function deriveAssociatedTokenAddress(tokenMint, owner) {
1585
+ const [address] = PublicKey.findProgramAddressSync([owner.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), tokenMint.toBuffer()], ASSOCIATED_TOKEN_PROGRAM_ID);
1586
+ return address;
1587
+ }
1588
+ function getEscrowPdas(client, agentWallet, depositorWallet, nonce) {
1589
+ const [agentPda] = Pda.deriveAgent(agentWallet, client.programId);
1590
+ const [escrowPda] = Pda.deriveEscrowV2(agentPda, depositorWallet, bnToPdaSeed(nonce, 'nonce'), client.programId);
1591
+ const [agentStake] = Pda.deriveStake(agentPda, client.programId);
1592
+ const [agentStats] = Pda.deriveAgentStats(agentPda, client.programId);
1593
+ const [pricingMenu] = Pda.derivePricingMenu(agentPda, client.programId);
1594
+ return { agentPda, escrowPda, agentStake, agentStats, pricingMenu };
1595
+ }
1596
+ function buildSplRemainingAccounts(tokenMint, sourceOwner, destinationOwner) {
1597
+ if (!tokenMint) {
1598
+ return [];
1599
+ }
1600
+ return [
1601
+ {
1602
+ pubkey: deriveAssociatedTokenAddress(tokenMint, sourceOwner),
1603
+ isSigner: false,
1604
+ isWritable: true,
1605
+ },
1606
+ {
1607
+ pubkey: deriveAssociatedTokenAddress(tokenMint, destinationOwner),
1608
+ isSigner: false,
1609
+ isWritable: true,
1610
+ },
1611
+ { pubkey: tokenMint, isSigner: false, isWritable: false },
1612
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
1613
+ ];
1614
+ }
1615
+ function buildEscrowSettlementRemainingAccounts(params) {
1616
+ const accounts = params.tokenMint
1617
+ ? [
1618
+ ...buildSplRemainingAccounts(params.tokenMint, params.escrowPda, params.agentWallet),
1619
+ {
1620
+ pubkey: deriveAssociatedTokenAddress(params.tokenMint, new PublicKey(SAP_PROTOCOL_TREASURY)),
1621
+ isSigner: false,
1622
+ isWritable: true,
1623
+ },
1624
+ ]
1625
+ : [
1626
+ {
1627
+ pubkey: new PublicKey(SAP_PROTOCOL_TREASURY),
1628
+ isSigner: false,
1629
+ isWritable: true,
1630
+ },
1631
+ ];
1632
+ if (params.pendingSettlementPda) {
1633
+ accounts.push({ pubkey: params.pendingSettlementPda, isSigner: false, isWritable: true });
1634
+ }
1635
+ if (params.coSigner) {
1636
+ accounts.push({ pubkey: params.coSigner, isSigner: true, isWritable: false });
1637
+ }
1638
+ return accounts;
1639
+ }
1640
+ async function serializeUnsignedTransaction(client, feePayer, instructions) {
1641
+ const latestBlockhash = await client.connection.getLatestBlockhash('confirmed');
1642
+ const tx = new Transaction();
1643
+ tx.feePayer = feePayer;
1644
+ tx.recentBlockhash = latestBlockhash.blockhash;
1645
+ for (const instruction of instructions) {
1646
+ tx.add(instruction);
1647
+ }
1648
+ return {
1649
+ transactionBase64: Buffer.from(tx.serialize({ requireAllSignatures: false })).toString('base64'),
1650
+ blockhash: latestBlockhash.blockhash,
1651
+ lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
1652
+ };
1653
+ }
1654
+ async function fetchEscrowV2Nullable(client, escrowPda) {
1655
+ const accounts = getSapAnchorAccounts(client);
1656
+ return accounts.escrowAccountV2.fetchNullable(escrowPda);
1657
+ }
1658
+ async function fetchPendingSettlementNullable(client, pendingPda) {
1659
+ const accounts = getSapAnchorAccounts(client);
1660
+ return accounts.pendingSettlement.fetchNullable(pendingPda);
1661
+ }
1662
+ function isDisputeWindowEscrow(escrow) {
1663
+ const security = escrow.settlementSecurity;
1664
+ return typeof security === 'object' && security !== null && 'disputeWindow' in security;
1665
+ }
1666
+ function isCoSignedEscrow(escrow) {
1667
+ const security = escrow.settlementSecurity;
1668
+ return typeof security === 'object' && security !== null && 'coSigned' in security;
1669
+ }
1670
+ function escrowBuilderResponse(params) {
1671
+ return {
1672
+ action: params.action,
1673
+ transactionBase64: params.transactionBase64,
1674
+ encoding: 'base64',
1675
+ requiredSigner: params.requiredSigner.toBase58(),
1676
+ requiredSignerRole: params.requiredSignerRole,
1677
+ submitWith: 'sap_payments_finalize_transaction',
1678
+ nextStep: 'Call sap_payments_finalize_transaction with transactionBase64, submit:true, confirm:true, and the same user-approved intent. Do not create temporary signing scripts and do not read keypair JSON.',
1679
+ accounts: {
1680
+ ...params.accounts,
1681
+ blockhash: params.blockhash,
1682
+ lastValidBlockHeight: params.lastValidBlockHeight,
1683
+ },
1684
+ tokenMode: params.tokenMint ? 'SPL' : 'SOL',
1685
+ ...(params.security ? { security: params.security } : {}),
1686
+ ...(params.warnings && params.warnings.length > 0 ? { warnings: params.warnings } : {}),
1687
+ };
1688
+ }
1689
+ async function buildEscrowCreateTransaction(input, client) {
1690
+ const depositorWallet = requiredPublicKey(input, 'depositorWallet');
1691
+ const agentWallet = requiredPublicKey(input, 'agentWallet');
1692
+ const args = parseEscrowV2Args(input);
1693
+ validateEscrowPaymentMint(args.tokenMint);
1694
+ const pdas = getEscrowPdas(client, agentWallet, depositorWallet, args.escrowNonce);
1695
+ const remainingAccounts = buildSplRemainingAccounts(args.tokenMint, depositorWallet, pdas.escrowPda);
1696
+ const methods = client.program.methods;
1697
+ const instruction = await methods.createEscrowV2(args.escrowNonce, args.pricePerCall, args.maxCalls, args.initialDeposit, args.expiresAt, args.volumeCurve, args.tokenMint, args.tokenDecimals, args.settlementSecurity, args.disputeWindowSlots, args.coSigner, args.arbiter)
1698
+ .accounts({
1699
+ depositor: depositorWallet,
1700
+ agent: pdas.agentPda,
1701
+ agentStake: pdas.agentStake,
1702
+ agentStats: pdas.agentStats,
1703
+ pricingMenu: pdas.pricingMenu,
1704
+ escrow: pdas.escrowPda,
1705
+ systemProgram: SystemProgram.programId,
1706
+ })
1707
+ .remainingAccounts(remainingAccounts)
1708
+ .instruction();
1709
+ const tx = await serializeUnsignedTransaction(client, depositorWallet, [instruction]);
1710
+ return escrowBuilderResponse({
1711
+ action: 'create_escrow_v2',
1712
+ transactionBase64: tx.transactionBase64,
1713
+ requiredSigner: depositorWallet,
1714
+ requiredSignerRole: 'depositor',
1715
+ tokenMint: args.tokenMint,
1716
+ blockhash: tx.blockhash,
1717
+ lastValidBlockHeight: tx.lastValidBlockHeight,
1718
+ accounts: {
1719
+ agentWallet,
1720
+ depositorWallet,
1721
+ agentPda: pdas.agentPda,
1722
+ escrowPda: pdas.escrowPda,
1723
+ agentStake: pdas.agentStake,
1724
+ agentStats: pdas.agentStats,
1725
+ pricingMenu: pdas.pricingMenu,
1726
+ remainingAccounts: serializeAccountMetas(remainingAccounts),
1727
+ },
1728
+ security: {
1729
+ settlementSecurity: args.settlementSecurity,
1730
+ disputeWindowSlots: args.disputeWindowSlots,
1731
+ coSigner: args.coSigner,
1732
+ arbiter: args.arbiter,
1733
+ },
1734
+ });
1735
+ }
1736
+ async function buildEscrowDepositTransaction(input, client) {
1737
+ const depositorWallet = requiredPublicKey(input, 'depositorWallet');
1738
+ const agentWallet = requiredPublicKey(input, 'agentWallet');
1739
+ const nonce = optionalBn(input, 'nonce', new BN(0));
1740
+ const amount = requiredBn(input, 'amount');
1741
+ const pdas = getEscrowPdas(client, agentWallet, depositorWallet, nonce);
1742
+ const escrow = await fetchEscrowV2Nullable(client, pdas.escrowPda);
1743
+ if (!escrow) {
1744
+ throw new Error(`Escrow V2 PDA ${pdas.escrowPda.toBase58()} does not exist. Build and finalize sap_escrow_build_create_transaction first.`);
1745
+ }
1746
+ const tokenMint = publicKeyOrNull(escrow.tokenMint);
1747
+ const remainingAccounts = buildSplRemainingAccounts(tokenMint, depositorWallet, pdas.escrowPda);
1748
+ const methods = client.program.methods;
1749
+ const instruction = await methods.depositEscrowV2(nonce, amount)
1750
+ .accounts({
1751
+ depositor: depositorWallet,
1752
+ escrow: pdas.escrowPda,
1753
+ systemProgram: SystemProgram.programId,
1754
+ })
1755
+ .remainingAccounts(remainingAccounts)
1756
+ .instruction();
1757
+ const tx = await serializeUnsignedTransaction(client, depositorWallet, [instruction]);
1758
+ return escrowBuilderResponse({
1759
+ action: 'deposit_escrow_v2',
1760
+ transactionBase64: tx.transactionBase64,
1761
+ requiredSigner: depositorWallet,
1762
+ requiredSignerRole: 'depositor',
1763
+ tokenMint,
1764
+ blockhash: tx.blockhash,
1765
+ lastValidBlockHeight: tx.lastValidBlockHeight,
1766
+ accounts: {
1767
+ agentWallet,
1768
+ depositorWallet,
1769
+ escrowPda: pdas.escrowPda,
1770
+ amount,
1771
+ remainingAccounts: serializeAccountMetas(remainingAccounts),
1772
+ },
1773
+ });
1774
+ }
1775
+ async function buildEscrowWithdrawTransaction(input, client) {
1776
+ const depositorWallet = requiredPublicKey(input, 'depositorWallet');
1777
+ const agentWallet = requiredPublicKey(input, 'agentWallet');
1778
+ const nonce = optionalBn(input, 'nonce', new BN(0));
1779
+ const amount = requiredBn(input, 'amount');
1780
+ const pdas = getEscrowPdas(client, agentWallet, depositorWallet, nonce);
1781
+ const escrow = await fetchEscrowV2Nullable(client, pdas.escrowPda);
1782
+ if (!escrow) {
1783
+ throw new Error(`Escrow V2 PDA ${pdas.escrowPda.toBase58()} does not exist.`);
1784
+ }
1785
+ const balance = bnToBigInt(escrow.balance);
1786
+ const pendingAmount = bnToBigInt(escrow.pendingAmount);
1787
+ const withdrawable = balance > pendingAmount ? balance - pendingAmount : 0n;
1788
+ if (BigInt(amount.toString(10)) > withdrawable) {
1789
+ throw new Error(`Withdraw amount exceeds withdrawable escrow balance. requested=${amount.toString(10)}, withdrawable=${withdrawable.toString()}.`);
1790
+ }
1791
+ const tokenMint = publicKeyOrNull(escrow.tokenMint);
1792
+ const remainingAccounts = buildSplRemainingAccounts(tokenMint, pdas.escrowPda, depositorWallet);
1793
+ const methods = client.program.methods;
1794
+ const instruction = await methods.withdrawEscrowV2(amount)
1795
+ .accounts({
1796
+ depositor: depositorWallet,
1797
+ escrow: pdas.escrowPda,
1798
+ })
1799
+ .remainingAccounts(remainingAccounts)
1800
+ .instruction();
1801
+ const tx = await serializeUnsignedTransaction(client, depositorWallet, [instruction]);
1802
+ return escrowBuilderResponse({
1803
+ action: 'withdraw_escrow_v2',
1804
+ transactionBase64: tx.transactionBase64,
1805
+ requiredSigner: depositorWallet,
1806
+ requiredSignerRole: 'depositor',
1807
+ tokenMint,
1808
+ blockhash: tx.blockhash,
1809
+ lastValidBlockHeight: tx.lastValidBlockHeight,
1810
+ accounts: {
1811
+ agentWallet,
1812
+ depositorWallet,
1813
+ escrowPda: pdas.escrowPda,
1814
+ amount,
1815
+ withdrawable: withdrawable.toString(),
1816
+ remainingAccounts: serializeAccountMetas(remainingAccounts),
1817
+ },
1818
+ });
1819
+ }
1820
+ async function buildEscrowSettleTransaction(input, client) {
1821
+ const agentWallet = requiredPublicKey(input, 'agentWallet');
1822
+ const depositorWallet = requiredPublicKey(input, 'depositorWallet');
1823
+ const nonce = optionalBn(input, 'nonce', new BN(0));
1824
+ const callsToSettle = requiredBn(input, 'callsToSettle');
1825
+ const serviceHash = requiredBytes(input, 'serviceHash', 32);
1826
+ const pdas = getEscrowPdas(client, agentWallet, depositorWallet, nonce);
1827
+ const escrow = await fetchEscrowV2Nullable(client, pdas.escrowPda);
1828
+ if (!escrow) {
1829
+ throw new Error(`Escrow V2 PDA ${pdas.escrowPda.toBase58()} does not exist.`);
1830
+ }
1831
+ const tokenMint = publicKeyOrNull(escrow.tokenMint);
1832
+ const pendingSettlementIndex = bnToBigInt(escrow.settlementIndex);
1833
+ let pendingSettlementPda;
1834
+ if (isDisputeWindowEscrow(escrow)) {
1835
+ [pendingSettlementPda] = Pda.derivePendingSettlement(pdas.escrowPda, pendingSettlementIndex, client.programId);
1836
+ const existing = await fetchPendingSettlementNullable(client, pendingSettlementPda);
1837
+ if (existing) {
1838
+ throw new Error(`Pending settlement ${pendingSettlementPda.toBase58()} already exists for settlementIndex=${pendingSettlementIndex.toString()}. Finalize or quarantine it before building another DisputeWindow settlement.`);
1839
+ }
1840
+ }
1841
+ const coSigner = isCoSignedEscrow(escrow) ? publicKeyOrNull(escrow.coSigner) : optionalPublicKey(input, 'coSigner') ?? null;
1842
+ if (isCoSignedEscrow(escrow) && !coSigner) {
1843
+ throw new Error('CoSigned escrow requires coSigner in the escrow account.');
1844
+ }
1845
+ const remainingAccounts = buildEscrowSettlementRemainingAccounts({
1846
+ tokenMint,
1847
+ escrowPda: pdas.escrowPda,
1848
+ agentWallet,
1849
+ pendingSettlementPda,
1850
+ coSigner,
1851
+ });
1852
+ const methods = client.program.methods;
1853
+ const instruction = await methods.settleCallsV2(nonce, callsToSettle, serviceHash)
1854
+ .accountsPartial({
1855
+ wallet: agentWallet,
1856
+ agent: pdas.agentPda,
1857
+ agentStats: pdas.agentStats,
1858
+ escrow: pdas.escrowPda,
1859
+ systemProgram: SystemProgram.programId,
1860
+ })
1861
+ .remainingAccounts(remainingAccounts)
1862
+ .instruction();
1863
+ const tx = await serializeUnsignedTransaction(client, agentWallet, [instruction]);
1864
+ return escrowBuilderResponse({
1865
+ action: 'settle_calls_v2',
1866
+ transactionBase64: tx.transactionBase64,
1867
+ requiredSigner: agentWallet,
1868
+ requiredSignerRole: 'agentWallet',
1869
+ tokenMint,
1870
+ blockhash: tx.blockhash,
1871
+ lastValidBlockHeight: tx.lastValidBlockHeight,
1872
+ accounts: {
1873
+ agentWallet,
1874
+ depositorWallet,
1875
+ agentPda: pdas.agentPda,
1876
+ agentStats: pdas.agentStats,
1877
+ escrowPda: pdas.escrowPda,
1878
+ pendingSettlementPda: pendingSettlementPda ?? null,
1879
+ pendingSettlementIndex: pendingSettlementIndex.toString(),
1880
+ callsToSettle,
1881
+ serviceHash,
1882
+ remainingAccounts: serializeAccountMetas(remainingAccounts),
1883
+ },
1884
+ security: {
1885
+ settlementSecurity: isDisputeWindowEscrow(escrow) ? ESCROW_V2_DISPUTE_WINDOW_MODE : ESCROW_V2_COSIGNED_MODE,
1886
+ coSigner,
1887
+ },
1888
+ });
1889
+ }
1890
+ async function buildEscrowFinalizeTransaction(input, client) {
1891
+ const payerWallet = requiredPublicKey(input, 'payerWallet');
1892
+ const agentWallet = requiredPublicKey(input, 'agentWallet');
1893
+ const depositorWallet = requiredPublicKey(input, 'depositorWallet');
1894
+ const nonce = optionalBn(input, 'nonce', new BN(0));
1895
+ const settlementIndex = requiredBn(input, 'settlementIndex');
1896
+ const pdas = getEscrowPdas(client, agentWallet, depositorWallet, nonce);
1897
+ const [pendingSettlementPda] = Pda.derivePendingSettlement(pdas.escrowPda, bnToPdaSeed(settlementIndex, 'settlementIndex'), client.programId);
1898
+ const escrow = await fetchEscrowV2Nullable(client, pdas.escrowPda);
1899
+ if (!escrow) {
1900
+ throw new Error(`Escrow V2 PDA ${pdas.escrowPda.toBase58()} does not exist.`);
1901
+ }
1902
+ const pending = await fetchPendingSettlementNullable(client, pendingSettlementPda);
1903
+ if (!pending) {
1904
+ throw new Error(`Pending settlement ${pendingSettlementPda.toBase58()} does not exist.`);
1905
+ }
1906
+ const tokenMint = publicKeyOrNull(escrow.tokenMint);
1907
+ const remainingAccounts = buildSplRemainingAccounts(tokenMint, pdas.escrowPda, agentWallet);
1908
+ const methods = client.program.methods;
1909
+ const instruction = await methods.finalizeSettlement()
1910
+ .accounts({
1911
+ payer: payerWallet,
1912
+ agentWallet,
1913
+ escrow: pdas.escrowPda,
1914
+ pendingSettlement: pendingSettlementPda,
1915
+ agentStats: pdas.agentStats,
1916
+ })
1917
+ .remainingAccounts(remainingAccounts)
1918
+ .instruction();
1919
+ const tx = await serializeUnsignedTransaction(client, payerWallet, [instruction]);
1920
+ return escrowBuilderResponse({
1921
+ action: 'finalize_settlement',
1922
+ transactionBase64: tx.transactionBase64,
1923
+ requiredSigner: payerWallet,
1924
+ requiredSignerRole: 'payer',
1925
+ tokenMint,
1926
+ blockhash: tx.blockhash,
1927
+ lastValidBlockHeight: tx.lastValidBlockHeight,
1928
+ accounts: {
1929
+ payerWallet,
1930
+ agentWallet,
1931
+ depositorWallet,
1932
+ escrowPda: pdas.escrowPda,
1933
+ pendingSettlementPda,
1934
+ settlementIndex,
1935
+ releaseSlot: pending.releaseSlot,
1936
+ remainingAccounts: serializeAccountMetas(remainingAccounts),
1937
+ },
1938
+ });
1939
+ }
1940
+ async function buildEscrowCloseTransaction(input, client) {
1941
+ const depositorWallet = requiredPublicKey(input, 'depositorWallet');
1942
+ const agentWallet = requiredPublicKey(input, 'agentWallet');
1943
+ const nonce = optionalBn(input, 'nonce', new BN(0));
1944
+ const pdas = getEscrowPdas(client, agentWallet, depositorWallet, nonce);
1945
+ const escrow = await fetchEscrowV2Nullable(client, pdas.escrowPda);
1946
+ if (!escrow) {
1947
+ throw new Error(`Escrow V2 PDA ${pdas.escrowPda.toBase58()} does not exist.`);
1948
+ }
1949
+ if (bnToBigInt(escrow.balance) !== 0n || bnToBigInt(escrow.pendingAmount) !== 0n) {
1950
+ throw new Error('Escrow cannot close until balance and pendingAmount are both zero.');
1951
+ }
1952
+ const methods = client.program.methods;
1953
+ const instruction = await methods.closeEscrowV2()
1954
+ .accounts({
1955
+ depositor: depositorWallet,
1956
+ escrow: pdas.escrowPda,
1957
+ agentStats: pdas.agentStats,
1958
+ })
1959
+ .instruction();
1960
+ const tx = await serializeUnsignedTransaction(client, depositorWallet, [instruction]);
1961
+ return escrowBuilderResponse({
1962
+ action: 'close_escrow_v2',
1963
+ transactionBase64: tx.transactionBase64,
1964
+ requiredSigner: depositorWallet,
1965
+ requiredSignerRole: 'depositor',
1966
+ tokenMint: publicKeyOrNull(escrow.tokenMint),
1967
+ blockhash: tx.blockhash,
1968
+ lastValidBlockHeight: tx.lastValidBlockHeight,
1969
+ accounts: {
1970
+ agentWallet,
1971
+ depositorWallet,
1972
+ escrowPda: pdas.escrowPda,
1973
+ agentStats: pdas.agentStats,
1974
+ },
1975
+ });
1976
+ }
888
1977
  /**
889
1978
  * @name parseFeedbackArgs
890
1979
  * @description Builds typed feedback args from MCP JSON input.
@@ -1000,7 +2089,7 @@ function buildSapSdkToolDescription(definition) {
1000
2089
  }
1001
2090
  function getSapSdkToolContext(name) {
1002
2091
  if (name === 'sap_register_agent') {
1003
- return 'SAP MCP context: This is the primary on-chain SAP agent registration tool for a local profile signer or external signer. Hosted accountless SAP MCP rejects direct registration before x402 payment because OOBE never custodies user wallet keys. Use agentUri or metadataUri for off-chain metadata, including a Metaplex or DAS-backed identity document when the agent also has NFT/collection metadata. After registration, use sap_publish_tool_by_name for advertised MCP capabilities and AgentKit metaplex-nft_* tools for NFT collection, badge, or metadata workflows.';
2092
+ return 'SAP MCP context: Do not use this raw SDK wrapper for production registration. The canonical registration path is sap_payments_register_agent, because it confirms the account, audits the protocol treasury fee, and fails closed when protocolComplete is false. Hosted accountless SAP MCP rejects direct registration before x402 payment because OOBE never custodies user wallet keys. Use sap_agent_identity_plan first, then sap_payments_register_agent with confirm:true.';
1004
2093
  }
1005
2094
  if (name === 'sap_update_agent') {
1006
2095
  return 'SAP MCP context: Use this after sap_register_agent to refresh name, description, capabilities, pricing, supported protocols, x402 endpoint, or metadataUri. For NFT-backed identity changes, update the Metaplex asset first when needed, then point the SAP agent metadataUri at the current metadata document.';
@@ -1008,6 +2097,9 @@ function getSapSdkToolContext(name) {
1008
2097
  if (name.startsWith('sap_publish_tool') || name.startsWith('sap_update_tool')) {
1009
2098
  return 'SAP MCP context: Use tool registry writes to advertise concrete capabilities that this MCP can serve, including AgentKit bridge tools such as bridging_bridgeWormhole and Metaplex tools such as metaplex-nft_mintNFT. Publish only schemas and descriptions that match the actual MCP tool surface.';
1010
2099
  }
2100
+ if (name.startsWith('sap_escrow_build_')) {
2101
+ return 'SAP MCP context: Hosted-safe unsigned Escrow V2 builder. The output is not submitted and is not signed by hosted SAP MCP. Preview it, then call local sap_payments_finalize_transaction with submit:true and confirm:true. Never create temporary signing scripts or read keypair JSON.';
2102
+ }
1011
2103
  if (name.startsWith('sap_discover') || name.startsWith('sap_list') || name.startsWith('sap_find') || name.startsWith('sap_fetch') || name.startsWith('sap_get') || name.startsWith('sap_is')) {
1012
2104
  return 'SAP MCP context: Read-only SAP SDK wrapper against the configured Solana RPC and SAP program. Use these reads to inspect current chain state before mutating registry, payment, reputation, memory, or tool accounts.';
1013
2105
  }
@@ -1026,28 +2118,34 @@ function getSapSdkToolContext(name) {
1026
2118
  return 'SAP MCP context: Direct synapse-sap-sdk wrapper served by this MCP. Read tools return on-chain state; write tools require signer policy, configured RPC, and the active SAP profile.';
1027
2119
  }
1028
2120
  const agentTools = [
2121
+ {
2122
+ name: 'sap_protocol_invariants',
2123
+ title: 'Get SAP Protocol Invariants',
2124
+ description: 'Free read-only protocol invariant card for agents. Returns the current SAP program id, protocol treasury, source-level expected 0.1 SOL registration fee invariant, hosted write routing rules, local sap_payments routes, identity pipeline, and forbidden actions. Use this before SAP registry writes and whenever fee/treasury behavior is unclear.',
2125
+ inputSchema: {},
2126
+ handler: async () => buildSapProtocolInvariants(),
2127
+ },
2128
+ {
2129
+ name: 'sap_agent_identity_plan',
2130
+ title: 'Plan SAP Agent Identity',
2131
+ description: 'Free read-only planner for SAP agent registration, profile/image updates, Metaplex/MPL Core identity linking, SNS linking, x402 pricing metadata, and post-write verification. Use this before sap_payments_register_agent or sap_payments_update_agent so agents know the exact local-signer route, metadata contract, forbidden actions, and protocol fee verification checklist. It does not touch chain and never signs.',
2132
+ inputSchema: SAP_AGENT_IDENTITY_PLAN_INPUT_SCHEMA,
2133
+ handler: async (input) => buildSapAgentIdentityPlan(input),
2134
+ },
1029
2135
  {
1030
2136
  name: 'sap_register_agent',
1031
- title: 'Register SAP Agent',
1032
- description: 'Local-signer-only: register the connected wallet as a SAP agent using SDK AgentModule.register. Hosted accountless SAP MCP rejects this direct write before x402 payment; run it through a local SAP MCP profile or external signer.',
1033
- inputSchema: {
1034
- name: { type: 'string', description: 'Human-readable name for the SAP agent' },
1035
- description: { type: 'string', description: 'Detailed description of the agent\'s purpose and capabilities' },
1036
- capabilities: { type: 'array', description: 'Array of capability objects or strings identifying what the agent can do (e.g. "jupiter:swap")' },
1037
- pricing: { type: 'array', description: 'Array of pricing tier objects defining per-call costs, rate limits, and settlement terms' },
1038
- protocols: { type: 'array', items: { type: 'string' }, description: 'Array of protocol identifiers the agent supports (e.g. "jupiter", "drift")' },
1039
- agentId: { type: 'string', description: 'Optional unique agent identifier string' },
1040
- agentUri: { type: 'string', description: 'Optional URI to the agent\'s off-chain metadata or service endpoint' },
1041
- metadataUri: { type: 'string', description: 'Alias for agentUri — URI to off-chain agent metadata JSON' },
1042
- x402Endpoint: { type: 'string', description: 'Optional x402 payment endpoint URL for HTTP-based agent payments' },
2137
+ title: 'Register SAP Agent (Raw SDK Deprecated)',
2138
+ description: 'Deprecated raw SDK wrapper. Production registration must use sap_payments_register_agent so the local signer path confirms the account, verifies the 0.1 SOL protocol treasury fee invariant, and returns protocolComplete. Hosted accountless SAP MCP rejects this direct write before x402 payment.',
2139
+ inputSchema: SAP_AGENT_REGISTER_INPUT_SCHEMA,
2140
+ handler: async () => {
2141
+ throw new Error('sap_register_agent raw SDK path is disabled for production safety. Call sap_agent_identity_plan, then sap_payments_register_agent with the same registration fields and confirm: true. success is true only when the agent account exists and protocolFee.status is verified.');
1043
2142
  },
1044
- handler: async (input, client) => ({ signature: await client.agent.register(parseRegisterAgentArgs(input)) }),
1045
2143
  },
1046
2144
  {
1047
2145
  name: 'sap_update_agent',
1048
2146
  title: 'Update SAP Agent',
1049
- description: 'Update the connected wallet SAP agent using SDK AgentModule.update.',
1050
- inputSchema: { type: 'object', additionalProperties: true, description: 'Object containing any combination of agent fields to update (name, description, capabilities, pricing, protocols, agentId, agentUri, x402Endpoint)' },
2147
+ description: 'Local-signer-only: update the connected wallet SAP agent using SDK AgentModule.update. Hosted accountless SAP MCP rejects this direct write before x402 payment; hosted users should call sap_payments_update_agent from the local sap_payments bridge.',
2148
+ inputSchema: SAP_AGENT_UPDATE_INPUT_SCHEMA,
1051
2149
  handler: async (input, client) => ({ signature: await client.agent.update(parseUpdateAgentArgs(input)) }),
1052
2150
  },
1053
2151
  {
@@ -1090,7 +2188,7 @@ const agentTools = [
1090
2188
  {
1091
2189
  name: 'sap_get_agent',
1092
2190
  title: 'Get SAP Agent',
1093
- description: 'Fetch agent identity by owner wallet. If omitted, fetches the connected wallet agent.',
2191
+ description: 'Free exact SAP agent identity read by owner wallet. If omitted, fetches the connected wallet agent. Use this before paid discovery when the wallet is known.',
1094
2192
  inputSchema: { wallet: { type: 'string', description: 'Solana public key of the agent owner wallet (base58). If omitted, uses the connected wallet.' } },
1095
2193
  handler: async (input, client) => {
1096
2194
  const wallet = optionalPublicKey(input, 'wallet');
@@ -1102,7 +2200,7 @@ const agentTools = [
1102
2200
  {
1103
2201
  name: 'sap_get_agent_stats',
1104
2202
  title: 'Get SAP Agent Stats',
1105
- description: 'Fetch agent stats by agent PDA.',
2203
+ description: 'Free exact SAP agent stats read by agent PDA. Use after sap_get_agent or sap_get_agent_profile when stats are needed.',
1106
2204
  inputSchema: { agentPda: { type: 'string', description: 'Agent PDA (base58) to fetch stats for' } },
1107
2205
  handler: async (input, client) => ({
1108
2206
  stats: await client.agent.fetchStatsNullable(requiredPublicKey(input, 'agentPda')),
@@ -1111,7 +2209,7 @@ const agentTools = [
1111
2209
  {
1112
2210
  name: 'sap_get_global_state',
1113
2211
  title: 'Get SAP Global State',
1114
- description: 'Fetch the on-chain global registry through SDK AgentModule.fetchGlobalRegistry.',
2212
+ description: 'Free compact global SAP registry state read. Use for initial orientation before paid network analytics.',
1115
2213
  inputSchema: {},
1116
2214
  handler: async (_input, client) => ({ state: await client.agent.fetchGlobalRegistry() }),
1117
2215
  },
@@ -1124,17 +2222,41 @@ const discoveryTools = [
1124
2222
  inputSchema: {},
1125
2223
  handler: async (_input, client) => ({ overview: await client.discovery.getNetworkOverview() }),
1126
2224
  },
2225
+ {
2226
+ name: 'sap_agent_context',
2227
+ title: 'Get SAP Agent Context',
2228
+ description: 'Free one-shot SAP orientation context for agents. Use this when the user asks whether SAP MCP is connected, wants to understand an agent/wallet, or needs the next safe paid/write route. It combines exact reads or a compact directory page with routing guidance without triggering x402.',
2229
+ inputSchema: {
2230
+ wallet: {
2231
+ type: 'string',
2232
+ description: 'Optional exact owner wallet public key (base58). When supplied, returns the matching agent identity, PDA, active state, and hydrated profile when available.',
2233
+ },
2234
+ agentPda: {
2235
+ type: 'string',
2236
+ description: 'Optional exact SAP agent PDA (base58) for compact context lookup when the owner wallet is not known.',
2237
+ },
2238
+ query: {
2239
+ type: 'string',
2240
+ description: 'Optional text query for a free compact orientation search. Keep this narrow, for example "XONA" or "Solking".',
2241
+ },
2242
+ limit: {
2243
+ type: 'number',
2244
+ description: 'Maximum compact orientation rows to include when wallet is not supplied. Defaults to 10 and is capped at 20 to keep this tool free.',
2245
+ },
2246
+ },
2247
+ handler: async (input, client) => buildSapAgentContext(input, client),
2248
+ },
1127
2249
  {
1128
2250
  name: 'sap_get_agent_profile',
1129
2251
  title: 'Get SAP Agent Profile',
1130
- description: 'Fetch a hydrated SAP agent profile by owner wallet.',
2252
+ description: 'Free exact SAP agent profile read by owner wallet. Use this for a known agent before paid discovery or enrichment.',
1131
2253
  inputSchema: { wallet: { type: 'string', description: 'Solana public key of the agent owner wallet (base58)' } },
1132
2254
  handler: async (input, client) => ({ profile: await client.discovery.getAgentProfile(requiredPublicKey(input, 'wallet')) }),
1133
2255
  },
1134
2256
  {
1135
2257
  name: 'sap_is_agent_active',
1136
2258
  title: 'Check SAP Agent Active',
1137
- description: 'Check if a wallet owns an active SAP agent.',
2259
+ description: 'Free exact activity check for an owner wallet. Use this before paid discovery when the wallet is known.',
1138
2260
  inputSchema: { wallet: { type: 'string', description: 'Solana public key of the wallet to check for an active SAP agent (base58)' } },
1139
2261
  handler: async (input, client) => ({ active: await client.discovery.isAgentActive(requiredPublicKey(input, 'wallet')) }),
1140
2262
  },
@@ -1147,7 +2269,7 @@ const discoveryTools = [
1147
2269
  const limit = Math.max(1, Math.min(optionalNumber(input, 'limit') ?? 50, 500));
1148
2270
  const capability = optionalString(input, 'capability');
1149
2271
  const capabilities = parseOptionalStringArray(input, 'capabilities');
1150
- return buildAgentDirectoryPage(client, {
2272
+ return getCachedAgentDirectoryPage(client, {
1151
2273
  includeInactive: input.includeInactive === true,
1152
2274
  protocol: optionalString(input, 'protocol'),
1153
2275
  capability,
@@ -1167,9 +2289,27 @@ const discoveryTools = [
1167
2289
  {
1168
2290
  name: 'sap_list_agents',
1169
2291
  title: 'List SAP Agents',
1170
- description: 'Compatibility alias for sap_discover_agents. Supports query, wallet, protocol, capability, x402 endpoint filtering, and cursor pagination.',
1171
- inputSchema: makeAgentDirectoryInputSchema(50),
1172
- handler: async (input, client) => discoveryTools[3].handler(input, client),
2292
+ description: 'Compact SAP agent orientation list. Free only when limit <= 20, view is compact, hydrate is false, and includeProtocolIndexes is false; larger or enriched pages are paid read-premium.',
2293
+ inputSchema: makeAgentDirectoryInputSchema(20),
2294
+ handler: async (input, client) => {
2295
+ const limit = Math.max(1, Math.min(optionalNumber(input, 'limit') ?? 20, 500));
2296
+ const capability = optionalString(input, 'capability');
2297
+ return getCachedAgentDirectoryPage(client, {
2298
+ includeInactive: input.includeInactive === true,
2299
+ protocol: optionalString(input, 'protocol'),
2300
+ capability,
2301
+ capabilities: parseOptionalStringArray(input, 'capabilities'),
2302
+ capabilityMode: parseCapabilityMode(input),
2303
+ query: optionalString(input, 'query'),
2304
+ wallet: optionalString(input, 'wallet'),
2305
+ agentPda: optionalString(input, 'agentPda'),
2306
+ hasX402Endpoint: parseHasX402Endpoint(input),
2307
+ limit,
2308
+ offset: parseDirectoryOffset(input),
2309
+ view: parseDirectoryView(input),
2310
+ includeProtocolIndexes: input.includeProtocolIndexes === true,
2311
+ });
2312
+ },
1173
2313
  },
1174
2314
  {
1175
2315
  name: 'sap_list_all_agents',
@@ -1179,7 +2319,7 @@ const discoveryTools = [
1179
2319
  handler: async (input, client) => {
1180
2320
  const limit = Math.max(1, Math.min(optionalNumber(input, 'limit') ?? 100, 500));
1181
2321
  const capability = optionalString(input, 'capability');
1182
- return buildAgentDirectoryPage(client, {
2322
+ return getCachedAgentDirectoryPage(client, {
1183
2323
  includeInactive: input.includeInactive === true,
1184
2324
  protocol: optionalString(input, 'protocol'),
1185
2325
  capability,
@@ -1457,24 +2597,66 @@ const paymentAndEscrowTools = [
1457
2597
  },
1458
2598
  ];
1459
2599
  const escrowV2Tools = [
2600
+ {
2601
+ name: 'sap_escrow_build_create_transaction',
2602
+ title: 'Build SAP Escrow V2 Create Transaction',
2603
+ description: 'Hosted-safe unsigned builder for create_escrow_v2. Use this from hosted SAP MCP, preview the result, then call local sap_payments_finalize_transaction with submit:true. The depositorWallet signs locally; keypair bytes never leave the user machine. Defaults to DisputeWindow settlementSecurity=2 and rejects SelfReport/0.',
2604
+ inputSchema: escrowV2CreateBuilderInputSchema,
2605
+ handler: buildEscrowCreateTransaction,
2606
+ },
2607
+ {
2608
+ name: 'sap_escrow_build_deposit_transaction',
2609
+ title: 'Build SAP Escrow V2 Deposit Transaction',
2610
+ description: 'Hosted-safe unsigned builder for deposit_escrow_v2. Use this when a hosted workflow needs to add funds to an existing V2 escrow, then finalize locally with sap_payments_finalize_transaction.',
2611
+ inputSchema: escrowV2DepositBuilderInputSchema,
2612
+ handler: buildEscrowDepositTransaction,
2613
+ },
2614
+ {
2615
+ name: 'sap_escrow_build_settle_transaction',
2616
+ title: 'Build SAP Escrow V2 Settlement Transaction',
2617
+ description: 'Hosted-safe unsigned builder for settle_calls_v2. The agent owner wallet signs locally. DisputeWindow escrows create a pending settlement PDA; CoSigned escrows require the coSigner account when configured.',
2618
+ inputSchema: escrowV2SettleBuilderInputSchema,
2619
+ handler: buildEscrowSettleTransaction,
2620
+ },
2621
+ {
2622
+ name: 'sap_escrow_build_finalize_transaction',
2623
+ title: 'Build SAP Escrow V2 Finalize Transaction',
2624
+ description: 'Hosted-safe unsigned builder for finalize_settlement. Use after the dispute window has elapsed and a pending settlement exists; any payerWallet can locally sign to crank finalization.',
2625
+ inputSchema: escrowV2FinalizeBuilderInputSchema,
2626
+ handler: buildEscrowFinalizeTransaction,
2627
+ },
2628
+ {
2629
+ name: 'sap_escrow_build_withdraw_transaction',
2630
+ title: 'Build SAP Escrow V2 Withdraw Transaction',
2631
+ description: 'Hosted-safe unsigned builder for withdraw_escrow_v2. The depositor signs locally and can only withdraw unlocked balance after pending amounts are excluded.',
2632
+ inputSchema: escrowV2WithdrawBuilderInputSchema,
2633
+ handler: buildEscrowWithdrawTransaction,
2634
+ },
2635
+ {
2636
+ name: 'sap_escrow_build_close_transaction',
2637
+ title: 'Build SAP Escrow V2 Close Transaction',
2638
+ description: 'Hosted-safe unsigned builder for close_escrow_v2. The depositor signs locally; the builder refuses to close until balance and pendingAmount are both zero.',
2639
+ inputSchema: escrowV2CloseBuilderInputSchema,
2640
+ handler: buildEscrowCloseTransaction,
2641
+ },
1460
2642
  {
1461
2643
  name: 'sap_create_escrow_v2',
1462
2644
  title: 'Create SAP Escrow V2',
1463
- description: 'Create a V2 escrow using SDK EscrowV2Module.create. Defaults to DisputeWindow settlementSecurity=2; SelfReport/0 is rejected.',
2645
+ description: 'Local-signer-only direct V2 escrow creation using SDK EscrowV2Module.create. Defaults to DisputeWindow settlementSecurity=2; SelfReport/0 is rejected. Hosted accountless SAP MCP rejects this before x402 payment; hosted users should call sap_escrow_build_create_transaction and finalize locally.',
1464
2646
  inputSchema: escrowV2CreateInputSchema,
1465
2647
  handler: async (input, client) => ({ signature: await client.escrowV2.create(requiredPublicKey(input, 'agentWallet'), parseEscrowV2Args(input)) }),
1466
2648
  },
1467
2649
  {
1468
2650
  name: 'sap_deposit_escrow_v2',
1469
2651
  title: 'Deposit SAP Escrow V2',
1470
- description: 'Deposit funds into a V2 escrow.',
2652
+ description: 'Local-signer-only direct deposit into a V2 escrow. Hosted users should call sap_escrow_build_deposit_transaction and finalize locally.',
1471
2653
  inputSchema: { agentWallet: { type: 'string', description: 'Agent wallet public key (base58)' }, nonce: { type: 'string', description: 'Escrow nonce (as a decimal string, default: 0)' }, amount: { type: 'string', description: `Deposit amount as a decimal string. ${ESCROW_AMOUNT_DESCRIPTION}` } },
1472
2654
  handler: async (input, client) => ({ signature: await client.escrowV2.deposit(requiredPublicKey(input, 'agentWallet'), optionalBn(input, 'nonce', new BN(0)), requiredBn(input, 'amount')) }),
1473
2655
  },
1474
2656
  {
1475
2657
  name: 'sap_settle_escrow_v2',
1476
2658
  title: 'Settle SAP Escrow V2',
1477
- description: 'Settle calls against a V2 escrow.',
2659
+ description: 'Local-signer-only direct settlement against a V2 escrow. Hosted users should call sap_escrow_build_settle_transaction and finalize locally.',
1478
2660
  inputSchema: { depositorWallet: { type: 'string', description: 'Depositor wallet public key (base58)' }, nonce: { type: 'string', description: 'Escrow nonce (as a decimal string, default: 0)' }, callsToSettle: { type: 'string', description: 'Number of calls to settle (as a decimal string)' }, serviceHash: { type: 'array', description: '32-byte service hash as a byte array, hex string, or base64 string' } },
1479
2661
  handler: async (input, client) => ({
1480
2662
  signature: await client.escrowV2.settle(requiredPublicKey(input, 'depositorWallet'), optionalBn(input, 'nonce', new BN(0)), requiredBn(input, 'callsToSettle'), requiredBytes(input, 'serviceHash', 32)),
@@ -1492,7 +2674,7 @@ const escrowV2Tools = [
1492
2674
  {
1493
2675
  name: 'sap_finalize_settlement_v2',
1494
2676
  title: 'Finalize SAP Escrow V2 Settlement',
1495
- description: 'Finalize a V2 pending settlement.',
2677
+ description: 'Local-signer-only direct finalization of a V2 pending settlement. Hosted users should call sap_escrow_build_finalize_transaction and finalize locally.',
1496
2678
  inputSchema: { agentWallet: { type: 'string', description: 'Agent wallet public key (base58)' }, depositorWallet: { type: 'string', description: 'Depositor wallet public key (base58)' }, nonce: { type: 'string', description: 'Escrow nonce (as a decimal string, default: 0)' }, settlementIndex: { type: 'string', description: 'Settlement index to finalize (as a decimal string)' } },
1497
2679
  handler: async (input, client) => ({
1498
2680
  signature: await client.escrowV2.finalizeSettlement(requiredPublicKey(input, 'agentWallet'), requiredPublicKey(input, 'depositorWallet'), optionalBn(input, 'nonce', new BN(0)), requiredBn(input, 'settlementIndex')),
@@ -1510,14 +2692,14 @@ const escrowV2Tools = [
1510
2692
  {
1511
2693
  name: 'sap_withdraw_escrow_v2',
1512
2694
  title: 'Withdraw SAP Escrow V2',
1513
- description: 'Withdraw funds from a V2 escrow.',
2695
+ description: 'Local-signer-only direct withdrawal from a V2 escrow. Hosted users should call sap_escrow_build_withdraw_transaction and finalize locally.',
1514
2696
  inputSchema: { agentWallet: { type: 'string', description: 'Agent wallet public key (base58)' }, nonce: { type: 'string', description: 'Escrow nonce (as a decimal string, default: 0)' }, amount: { type: 'string', description: `Withdrawal amount as a decimal string. ${ESCROW_AMOUNT_DESCRIPTION}` } },
1515
2697
  handler: async (input, client) => ({ signature: await client.escrowV2.withdraw(requiredPublicKey(input, 'agentWallet'), optionalBn(input, 'nonce', new BN(0)), requiredBn(input, 'amount')) }),
1516
2698
  },
1517
2699
  {
1518
2700
  name: 'sap_close_escrow_v2',
1519
2701
  title: 'Close SAP Escrow V2',
1520
- description: 'Close a V2 escrow.',
2702
+ description: 'Local-signer-only direct close for an empty V2 escrow. Hosted users should call sap_escrow_build_close_transaction and finalize locally.',
1521
2703
  inputSchema: { agentWallet: { type: 'string', description: 'Agent wallet public key (base58)' }, nonce: { type: 'string', description: 'Escrow nonce (as a decimal string, default: 0)' } },
1522
2704
  handler: async (input, client) => ({ signature: await client.escrowV2.close(requiredPublicKey(input, 'agentWallet'), optionalBn(input, 'nonce', new BN(0))) }),
1523
2705
  },