@zktx.io/sui-mcp 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +6 -6
  2. package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +3 -3
  3. package/dist/core/activity/localDataService.js +71 -52
  4. package/dist/core/activity/localDataValidation.js +1 -3
  5. package/dist/core/activity/sqliteActivityStore.js +24 -16
  6. package/dist/core/activity/sqliteActivityStoreSchema.js +83 -12
  7. package/dist/core/preferences/sqlitePreferencesRepository.js +5 -3
  8. package/dist/core/session/cardSession.js +1 -1
  9. package/dist/core/session/cardSessionStore.js +53 -3
  10. package/dist/core/session/sessionStore.js +0 -17
  11. package/dist/core/session/settingsFileIdentity.js +11 -0
  12. package/dist/core/session/settingsFileProtocol.js +25 -0
  13. package/dist/core/session/settingsFileStore.js +346 -0
  14. package/dist/core/session/settingsInputError.js +34 -0
  15. package/dist/core/session/settingsOperation.js +224 -0
  16. package/dist/core/session/settingsPreparationSql.js +15 -0
  17. package/dist/core/session/settingsWorkflow.js +1219 -0
  18. package/dist/core/session/sqliteSessionStore.js +0 -40
  19. package/dist/core/session/sqliteSettingsWorkflowStore.js +463 -0
  20. package/dist/core/session/sqliteWalletWorkflowStore.js +164 -41
  21. package/dist/core/session/status.js +2 -2
  22. package/dist/core/session/walletConnection.js +27 -8
  23. package/dist/core/session/walletWorkflow.js +73 -15
  24. package/dist/core/session/workflowState.js +16 -8
  25. package/dist/core/session/workflowView.js +18 -2
  26. package/dist/mcp/responseGuidance.js +10 -5
  27. package/dist/mcp/serverInfo.js +3 -4
  28. package/dist/mcp/toolNames.js +2 -2
  29. package/dist/mcp/tools/account/index.js +1 -19
  30. package/dist/mcp/tools/read/readToolHelpers.js +5 -4
  31. package/dist/mcp/tools/session/executionResultTools.js +2 -2
  32. package/dist/mcp/tools/session/shared.js +1 -0
  33. package/dist/mcp/tools/session/statusTools.js +4 -3
  34. package/dist/mcp/tools/settings/index.js +8 -28
  35. package/dist/mcp-app/account.html +14 -14
  36. package/dist/mcp-app/account.notices.txt +1 -1
  37. package/dist/mcp-app/chart.html +9 -9
  38. package/dist/mcp-app/chart.notices.txt +1 -1
  39. package/dist/mcp-app/connect.html +14 -14
  40. package/dist/mcp-app/connect.notices.txt +1 -1
  41. package/dist/mcp-app/receipt.html +1298 -1463
  42. package/dist/mcp-app/receipt.notices.txt +226 -1204
  43. package/dist/mcp-app/review.html +1298 -1463
  44. package/dist/mcp-app/review.notices.txt +226 -1204
  45. package/dist/mcp-app/settings.html +3133 -0
  46. package/dist/mcp-app/settings.notices.txt +973 -0
  47. package/dist/mcp-ui/contracts.js +8 -2
  48. package/dist/mcp-ui/readCards.js +1 -1
  49. package/dist/mcp-ui/tools.js +33 -2
  50. package/dist/review-server/server.js +5 -51
  51. package/dist/runtime/application.js +41 -14
  52. package/dist/runtime/localSettingsService.js +26 -2
  53. package/dist/runtime/shared/authenticatedFetch.js +6 -2
  54. package/dist/runtime/shared/control.js +5 -6
  55. package/dist/runtime/shared/http.js +14 -0
  56. package/dist/runtime/shared/mcpHttp.js +9 -4
  57. package/dist/runtime/shared/server.js +1 -1
  58. package/dist/runtime/smokeMainnetRead.js +0 -1
  59. package/dist/runtime/walletConnectStorage.js +2 -4
  60. package/dist/runtime/walletSdkChildRuntime.js +2 -1
  61. package/dist/runtime/walletSdkIpc.js +2 -1
  62. package/dist/runtime/walletSdkProcess.js +3 -2
  63. package/docs/AGENT_BEHAVIOR.md +44 -10
  64. package/docs/AGENT_DEVELOPMENT_POLICY.md +16 -12
  65. package/docs/FRONTEND_POLICY.md +105 -28
  66. package/docs/LOCAL_DB_ARCHITECTURE.md +143 -20
  67. package/docs/MCP_SETUP.md +99 -39
  68. package/docs/MCP_TOOLS.md +273 -24
  69. package/docs/SDK_API.md +51 -10
  70. package/docs/UTILITY_INDEX.md +9 -6
  71. package/docs/WALLET_CONNECTION.md +30 -9
  72. package/package.json +13 -7
  73. package/dist/core/activity/schemaVersion.js +0 -2
  74. package/dist/core/session/keyedRecordStore.js +0 -18
  75. package/dist/core/session/settingsSession.js +0 -1
  76. package/dist/core/session/settingsSessions.js +0 -45
  77. package/dist/review-app/brand-dark.svg +0 -1
  78. package/dist/review-app/brand-light.svg +0 -1
  79. package/dist/review-app/favicon.svg +0 -1
  80. package/dist/review-app/settings.css +0 -1
  81. package/dist/review-app/settings.js +0 -1
  82. package/dist/review-app/ui.css +0 -809
  83. package/dist/review-server/assets.js +0 -48
  84. package/dist/review-server/html.js +0 -45
  85. package/dist/review-server/http.js +0 -47
  86. package/dist/review-server/middleware/reviewToken.js +0 -7
  87. package/dist/review-server/settingsApi.js +0 -158
@@ -1,3 +1,4 @@
1
+ import { workflowConnectionEligibility } from "./workflowState.js";
1
2
  import { SessionStoreError } from "./sessionErrors.js";
2
3
  import { Transaction } from "@mysten/sui/transactions";
3
4
  import { verifyTransactionSignature } from "@mysten/sui/verify";
@@ -18,7 +19,11 @@ export class WalletWorkflow {
18
19
  observing = new Map();
19
20
  preparing = new Set();
20
21
  pendingWrites = new Map();
22
+ // Child exit ends child I/O, not verification or submit work already running
23
+ // in this parent. The owning signature continuation alone ends that work.
24
+ parentSignatureWork = new Set();
21
25
  eventListener;
26
+ settingsRecovery;
22
27
  constructor(options) {
23
28
  this.options = options;
24
29
  }
@@ -48,6 +53,7 @@ export class WalletWorkflow {
48
53
  if (this.started)
49
54
  throw new WorkflowConflict("The wallet service has already started.");
50
55
  this.started = true;
56
+ this.options.records.setWalletRunId(this.options.runtime.runId);
51
57
  this.options.records.recover(this.now());
52
58
  this.bindEvents();
53
59
  this.options.runtime.start((event) => this.eventListener?.(event));
@@ -61,9 +67,11 @@ export class WalletWorkflow {
61
67
  return;
62
68
  const runtime = this.options.runtime;
63
69
  if (event.type === "snapshot") {
64
- this.options.records.applyWalletObservation(event.snapshot, event.previous, event.ready);
65
- if (event.ready)
70
+ this.options.records.applyWalletObservation(event.snapshot, event.previous, event.ready, () => this.settingsRecovery?.runtimeEvent(event));
71
+ if (event.ready) {
66
72
  runtime.publishReady(event.runId);
73
+ this.settingsRecovery?.readyPublished(event.runId);
74
+ }
67
75
  }
68
76
  else if (event.type === "stage") {
69
77
  this.options.records.publishWalletState();
@@ -71,8 +79,38 @@ export class WalletWorkflow {
71
79
  else if (event.type === "lost") {
72
80
  this.pairings.clear();
73
81
  this.retainWrite({ kind: "runtime_failure", runId: event.runId, observedAt: this.now().toISOString() });
82
+ this.settingsRecovery?.runtimeEvent(event);
83
+ }
84
+ else if (event.type === "exit") {
85
+ this.retainWrite({ kind: "runtime_exit", runId: event.runId, observedAt: this.now().toISOString(), parentProcessing: [...this.parentSignatureWork] });
74
86
  }
75
87
  }
88
+ attachSettingsRecovery(recovery) {
89
+ if (this.settingsRecovery)
90
+ throw new Error("Settings recovery is already attached.");
91
+ this.settingsRecovery = recovery;
92
+ }
93
+ recoveryContext() {
94
+ this.synchronizeWalletState();
95
+ return this.currentRecoveryFacts();
96
+ }
97
+ currentRecoveryFacts() {
98
+ return { availability: this.walletAvailability(), impact: this.options.records.recoveryImpact(this.options.runtime.runId) };
99
+ }
100
+ blockSettingsAdmissionFailure() {
101
+ const runId = this.options.runtime.runId;
102
+ this.options.runtime.block("wallet_state_unavailable");
103
+ this.retainWrite({ kind: "runtime_failure", runId, observedAt: this.now().toISOString() });
104
+ }
105
+ fenceForSettings(runId) { this.assertCurrent(); this.options.runtime.fence(runId); }
106
+ replaceForSettings(runId, reserve) {
107
+ this.assertCurrent();
108
+ return this.options.runtime.replace(runId, (nextRunId) => {
109
+ this.synchronizeWalletState();
110
+ reserve(nextRunId);
111
+ this.options.records.setWalletRunId(nextRunId);
112
+ });
113
+ }
76
114
  dataReplaced() {
77
115
  this.pairings.clear();
78
116
  this.pendingWrites.clear();
@@ -99,7 +137,7 @@ export class WalletWorkflow {
99
137
  catch {
100
138
  return;
101
139
  }
102
- const key = completion.kind === "runtime_failure" ? `runtime:${completion.runId}` :
140
+ const key = completion.kind === "runtime_failure" || completion.kind === "runtime_exit" ? `${completion.kind}:${completion.runId}` :
103
141
  completion.kind === "request" ? `request:${completion.id}:${completion.field}` : `connection:${completion.cardId}:${completion.runId}`;
104
142
  if (!this.pendingWrites.has(key))
105
143
  this.pendingWrites.set(key, completion);
@@ -115,7 +153,9 @@ export class WalletWorkflow {
115
153
  this.options.records.settle(completion.id, completion.field, false);
116
154
  else if (completion.kind === "connection")
117
155
  this.options.records.settleConnection(completion.id, completion);
118
- else if (completion.runId === this.options.runtime.runId)
156
+ else if (completion.kind === "runtime_exit")
157
+ this.options.records.recordRunExit(completion.runId, completion.parentProcessing);
158
+ else
119
159
  this.options.records.recordWalletFailure(completion);
120
160
  this.pendingWrites.delete(key);
121
161
  }
@@ -137,6 +177,8 @@ export class WalletWorkflow {
137
177
  // read recorded facts. Retain failed repairs for the next applicable read.
138
178
  if (failure && walletDependent)
139
179
  throw failure;
180
+ if (walletDependent)
181
+ this.settingsRecovery?.synchronize();
140
182
  if (walletDependent)
141
183
  this.options.runtime.synchronize();
142
184
  }
@@ -159,16 +201,27 @@ export class WalletWorkflow {
159
201
  }
160
202
  return { status: "ready" };
161
203
  }
162
- readConnectionContext() {
204
+ reconcileConnectionContext() {
163
205
  this.assertCurrent();
164
206
  this.synchronizeWalletState();
165
207
  this.recoverDisconnects();
166
- const state = this.options.records.evaluate({ walletAvailability: this.walletAvailability(), walletObservation: this.observation() });
167
- // Current-owner rows are published by the same SDK observation/admission
168
- // transactions that gate availability. Use their evaluated selection in
169
- // every consumer instead of deriving another default from address matches.
170
- return { connections: state.connections, connectionConflict: state.connectionConflict, walletAvailability: state.walletAvailability, walletObservation: this.observation(),
171
- assetReadAccount: state.assetReadAccount ?? { status: "address_required" } };
208
+ const state = this.options.records.reconcileAndEvaluate({ purpose: "current_wallet", walletAvailability: this.walletAvailability(), walletObservation: this.observation() });
209
+ return { connections: state.connections, connectionConflict: state.connectionConflict, walletAvailability: state.walletAvailability,
210
+ walletObservation: state.walletObservation, manageableConnectionIds: state.manageableConnectionIds,
211
+ connectionReconciliationRequired: state.connectionReconciliationRequired, assetReadAccount: state.assetReadAccount ?? { status: "address_required" } };
212
+ }
213
+ // Diagnostic facts are collected without maintenance. Reconciliation and
214
+ // admission own durable transitions and cannot use this snapshot as authority.
215
+ currentConnectionContext() {
216
+ this.assertCurrent();
217
+ const facts = this.options.records.readConnectionFacts(this.now()), availability = this.walletAvailability();
218
+ const { selection: _selection, ...eligibility } = workflowConnectionEligibility({ ...facts, walletAvailability: availability });
219
+ // The shared projection gates qualification on published availability;
220
+ // a memory snapshot alone does not prove its journal/result has committed.
221
+ return { connections: facts.connections, walletAvailability: availability, walletObservation: facts.walletObservation,
222
+ connectionConflict: eligibility.connectionConflict, manageableConnectionIds: eligibility.manageableConnectionIds,
223
+ connectionReconciliationRequired: eligibility.connectionReconciliationRequired,
224
+ assetReadAccount: eligibility.assetReadAccount ?? { status: "address_required" } };
172
225
  }
173
226
  recoverDisconnects() {
174
227
  const available = this.walletAvailability().status === "available";
@@ -194,7 +247,9 @@ export class WalletWorkflow {
194
247
  this.options.records.failPreparation(id, preparation, this.now());
195
248
  const candidate = await this.options.sessions.inspectReview(id, this.now());
196
249
  this.assertCurrent();
197
- const evaluated = this.options.records.evaluate({ reviewSessionId: id, candidate, expectedCard, readTarget,
250
+ if (!candidate)
251
+ throw new SessionStoreError("session_not_found", "The saved review is unavailable.");
252
+ const evaluated = this.options.records.reconcileAndEvaluate({ purpose: "review", reviewSessionId: id, candidate, expectedCard, readTarget,
198
253
  walletAvailability: this.walletAvailability(), walletObservation: this.observation(), uiObservation });
199
254
  this.options.sessions.recordEvaluationEvents(evaluated.events);
200
255
  return evaluated;
@@ -216,15 +271,16 @@ export class WalletWorkflow {
216
271
  if (!state.session)
217
272
  return undefined;
218
273
  return { session: state.session, request: state.request, hasReviewInput: state.hasReviewInput, connectionConflict: state.connectionConflict, walletAvailability: state.walletAvailability, walletObservation: this.observation(),
274
+ connectionReconciliationRequired: state.connectionReconciliationRequired,
219
275
  progress: reviewProgress(!!state.session.preparationId, state.request, state.walletAvailability, { stopped: state.authority?.observation_stopped === 1, pending: state.authority?.lookup_pending === 1 }) };
220
276
  }
221
277
  async describe(record, uiObservation = false) {
222
278
  this.assertCurrent();
223
279
  if (record.state.kind === "connect")
224
- this.readConnectionContext();
280
+ this.reconcileConnectionContext();
225
281
  const evaluated = record.state.kind === "review"
226
282
  ? await this.evaluateReview(String(record.state.input.reviewSessionId), record, uiObservation)
227
- : this.options.records.evaluate({ expectedCard: record, walletAvailability: this.walletAvailability(), walletObservation: this.observation() });
283
+ : this.options.records.reconcileAndEvaluate({ purpose: "current_wallet", expectedCard: record, walletAvailability: this.walletAvailability(), walletObservation: this.observation() });
228
284
  this.assertCurrent();
229
285
  if (record.state.kind === "connect") {
230
286
  const data = projectConnectionView(evaluated);
@@ -444,6 +500,7 @@ export class WalletWorkflow {
444
500
  await this.performSignature(request, connectionId, bytesBase64, run);
445
501
  }
446
502
  finally {
503
+ this.parentSignatureWork.delete(request.attemptId);
447
504
  this.completeCallback({ kind: "request", id: request.attemptId, field: "sdk_pending" });
448
505
  }
449
506
  }
@@ -458,6 +515,7 @@ export class WalletWorkflow {
458
515
  this.assertWalletAvailable(run);
459
516
  response = await run.sign({ topic: connection.topic, account: request.account, transactionBytesBase64: bytesBase64,
460
517
  sessionVersion: proof.observed.version }, request.attemptId);
518
+ this.parentSignatureWork.add(request.attemptId);
461
519
  this.assertWalletAvailable(run);
462
520
  }
463
521
  catch {
@@ -581,7 +639,7 @@ export class WalletWorkflow {
581
639
  this.recoverDisconnects();
582
640
  const rows = [];
583
641
  for (const card of this.options.records.pendingConnectionCards(this.now())) {
584
- const evaluated = this.options.records.evaluate({ expectedCard: card, walletAvailability: this.walletAvailability(), walletObservation: this.observation() });
642
+ const evaluated = this.options.records.reconcileAndEvaluate({ purpose: "current_wallet", expectedCard: card, walletAvailability: this.walletAvailability(), walletObservation: this.observation() });
585
643
  if (evaluated.record?.state.state === "closed")
586
644
  continue;
587
645
  const data = projectConnectionView(evaluated);
@@ -15,6 +15,18 @@ export function reviewPreparationAccount(boundAccount, activeAccount, selectedAc
15
15
  message: "The selected account has changed since this request was made." };
16
16
  return { allowed: true, account: activeAccount };
17
17
  }
18
+ export function workflowConnectionEligibility(facts) {
19
+ const selection = walletConnectionSelection(facts.connections, Date.parse(facts.evaluatedAt), facts.connectionPresence, facts.activeAccount ? { address: facts.activeAccount, walletId: facts.activeAccountConnectionId } : undefined);
20
+ const available = facts.walletAvailability.status === "available", dependent = facts.walletDependent !== false;
21
+ const usableConnectionId = available && dependent ? selection.connection?.connectionId : undefined;
22
+ const assetReadAccount = !dependent ? undefined
23
+ : usableConnectionId ? selection.assetReadAccount : { status: "address_required" };
24
+ return { selection, usableConnectionId, assetReadAccount, connectionConflict: selection.conflict,
25
+ manageableConnectionIds: available ? selection.manageableConnectionIds : [],
26
+ // A pending memory snapshot is not a published SDK observation. Startup
27
+ // and state-sync failures retain their own result and neutral diagnosis.
28
+ connectionReconciliationRequired: available && dependent ? selection.connectionReconciliationRequired : undefined };
29
+ }
18
30
  // Shared domain eligibility. The DB supplies current facts at its decision
19
31
  // boundary. Returned choices are advisory; admission still binds concrete input.
20
32
  export function workflowEligibility(facts) {
@@ -27,12 +39,8 @@ export function workflowEligibility(facts) {
27
39
  ? Math.min(cardRemaining, Math.max(0, Date.parse(session.expiresAt) - at)) : cardRemaining;
28
40
  const inputAvailable = remaining > 0 && record?.state.state === "ready" && record.acceptedInput === undefined;
29
41
  const allowedActions = [];
30
- const selection = walletConnectionSelection(facts.connections, at, facts.activeAccount
31
- ? { address: facts.activeAccount, walletId: facts.activeAccountConnectionId } : undefined);
32
- const usableConnectionId = walletAvailability.status === "available" && facts.walletDependent !== false
33
- ? selection.connection?.connectionId : undefined;
34
- const assetReadAccount = facts.walletDependent === false ? undefined
35
- : usableConnectionId ? selection.assetReadAccount : { status: "address_required" };
42
+ const { selection, ...connectionEligibility } = workflowConnectionEligibility(facts);
43
+ const { assetReadAccount } = connectionEligibility;
36
44
  let preparationIssue;
37
45
  if (record?.scope === "connect") {
38
46
  if (inputAvailable) {
@@ -40,7 +48,7 @@ export function workflowEligibility(facts) {
40
48
  if (walletAvailability.status === "available") {
41
49
  if (["connect", "manage"].includes(String(record.state.input.intent)) && selection.pairingAllowed)
42
50
  allowedActions.push("connect");
43
- if (facts.connections.some((item) => item.status === "connected" && !item.pendingAction))
51
+ if (selection.manageableConnectionIds.length)
44
52
  allowedActions.push("disconnect");
45
53
  if (selection.connection)
46
54
  allowedActions.push("use_account");
@@ -74,7 +82,7 @@ export function workflowEligibility(facts) {
74
82
  session.status === "ready_for_wallet_review" && (!request || request.reviewRevision !== session.reviewRevision)
75
83
  ? session.reviewState?.humanReadableReview?.freshness.expiresAt : undefined;
76
84
  const nextRead = expiry && session ? Math.min(remaining, Date.parse(session.expiresAt) - at, Date.parse(expiry) - at) : undefined;
77
- return { allowedActions, preparationIssue, usableConnectionId, assetReadAccount, connectionConflict: selection.conflict,
85
+ return { allowedActions, preparationIssue, ...connectionEligibility,
78
86
  actionRemainingMs: remaining, inputRemainingMs: inputAvailable ? remaining : 0,
79
87
  ...(nextRead !== undefined && nextRead > 0 ? { nextStateReadAfterMs: nextRead } : {}) };
80
88
  }
@@ -37,6 +37,8 @@ export const workflowViewSchema = z.object({
37
37
  walletObservation: walletObservationSchema.optional(),
38
38
  runtimeRecovery: walletRecoverySchema.optional(),
39
39
  connectionConflict: connectionConflictSchema.optional(),
40
+ manageableConnectionIds: z.array(z.string().min(1)),
41
+ connectionReconciliationRequired: z.literal(true).optional(),
40
42
  usableConnectionId: z.string().min(1).optional(),
41
43
  assetReadAccount: assetReadAccountSchema.optional(),
42
44
  acceptedWalletRunId: walletRunIdSchema.optional(),
@@ -60,10 +62,19 @@ export const workflowViewSchema = z.object({
60
62
  }).strict().superRefine((view, context) => {
61
63
  const connection = view.connections.find((item) => item.connectionId === view.usableConnectionId);
62
64
  if (view.usableConnectionId && (!connection || connection.status !== "connected" ||
63
- view.walletAvailability.status !== "available" || view.connectionConflict ||
65
+ view.walletAvailability.status !== "available" || view.connectionConflict || view.connectionReconciliationRequired ||
64
66
  view.connections.some((item) => item.pendingAction || item.status === "awaiting_approval"))) {
65
67
  context.addIssue({ code: "custom", path: ["usableConnectionId"], message: "The usable connection must match current connection facts." });
66
68
  }
69
+ if (new Set(view.manageableConnectionIds).size !== view.manageableConnectionIds.length ||
70
+ view.manageableConnectionIds.some((id) => !view.connections.some((item) => item.connectionId === id &&
71
+ !item.pendingAction && item.status !== "awaiting_approval"))) {
72
+ context.addIssue({ code: "custom", path: ["manageableConnectionIds"], message: "Management must name distinct recorded targets without pending operations." });
73
+ }
74
+ if (view.connectionReconciliationRequired && (view.usableConnectionId || view.assetReadAccount?.status === "available" ||
75
+ view.allowedActions.some((action) => ["connect", "use_account", "prepare_review", "request_signature"].includes(action)))) {
76
+ context.addIssue({ code: "custom", path: ["connectionReconciliationRequired"], message: "Unrecorded SDK connections cannot authorize connection or account use." });
77
+ }
67
78
  if (view.assetReadAccount?.status === "available" && (!connection ||
68
79
  !connection.accounts.includes(view.assetReadAccount.account) || view.activeAccount !== view.assetReadAccount.account)) {
69
80
  context.addIssue({ code: "custom", path: ["assetReadAccount"], message: "The default address must belong to the usable connection and stored selection." });
@@ -104,7 +115,10 @@ function nextStateRead(input) {
104
115
  // These projections do not read clocks, storage, or admission rules. The
105
116
  // evaluated facts and choices were collected together by the database owner.
106
117
  export function projectConnectionView(input) {
107
- const { connection, walletAvailability } = input;
118
+ const { walletAvailability } = input;
119
+ const stored = input.record?.state.data;
120
+ const connection = input.record?.acceptedInput?.action === "disconnect" && stored?.disconnectOutcome
121
+ ? connectionViewSchema.parse(stored.disconnectOutcome) : input.connection;
108
122
  const readAfter = nextStateRead(input);
109
123
  const progress = input.runtimeRecovery ? { status: "phase" in input.runtimeRecovery ? "waiting" : "idle" } :
110
124
  workflowProgress(connection?.status === "awaiting_approval" || connection?.pendingAction === "disconnect", true, walletAvailability);
@@ -114,6 +128,7 @@ export function projectConnectionView(input) {
114
128
  ? { automaticAction: { action: "connect", walletRunId: walletAvailability.walletRunId } } : {}),
115
129
  runtimeRecovery: input.runtimeRecovery, walletObservation: input.walletObservation,
116
130
  connectionConflict: input.connectionConflict, usableConnectionId: input.usableConnectionId, assetReadAccount: input.assetReadAccount,
131
+ manageableConnectionIds: input.manageableConnectionIds, connectionReconciliationRequired: input.connectionReconciliationRequired,
117
132
  connectionAction: ["connect", "disconnect"].includes(String(input.record?.acceptedInput?.action)) ? input.record?.acceptedInput?.action : undefined,
118
133
  acceptedWalletRunId: input.record?.acceptedInput?.walletRunId,
119
134
  ...(readAfter === undefined ? {} : { nextStateReadAfterMs: readAfter }),
@@ -133,6 +148,7 @@ export function projectReviewView(input) {
133
148
  return workflowViewSchema.parse({ kind: "review", mode: record.scope === "review_manage" ? "review_manage" : "review",
134
149
  allowedActions: input.allowedActions, actionRemainingMs: input.actionRemainingMs, observe: progress.status === "waiting", progress, walletAvailability,
135
150
  connections: input.connections, connectionConflict: input.connectionConflict, usableConnectionId: input.usableConnectionId,
151
+ manageableConnectionIds: input.manageableConnectionIds, connectionReconciliationRequired: input.connectionReconciliationRequired,
136
152
  assetReadAccount: input.assetReadAccount, activeAccount: input.activeAccount, boundary: REVIEW_BOUNDARY, receipt: input.receipt,
137
153
  walletObservation: input.walletObservation,
138
154
  acceptedWalletRunId: record.acceptedInput?.walletRunId,
@@ -1,6 +1,6 @@
1
1
  import { EXTERNAL_PROPOSAL_SETTLEMENT_TOKEN_SELECTION_UNSUPPORTED_CLAIM_ID } from "../core/proposal/types.js";
2
2
  import { TOOL_NAMES } from "./toolNames.js";
3
- export function interactionStatusUserAnswerUse(hasWalletObservation = false, hasConnectionConflict = false) {
3
+ export function interactionStatusUserAnswerUse(hasWalletObservation = false, hasConnectionConflict = false, reconciliationRequired = false) {
4
4
  return {
5
5
  canAnswer: [
6
6
  "stored_active_account_read_context",
@@ -8,6 +8,7 @@ export function interactionStatusUserAnswerUse(hasWalletObservation = false, has
8
8
  "connection_qualified_default_asset_account",
9
9
  "pending_local_wallet_connection_interactions",
10
10
  ...(hasConnectionConflict ? ["multiple_saved_connections_require_user_targeted_disconnection"] : []),
11
+ ...(reconciliationRequired ? ["unrecorded_service_connection_requires_wallet_removal_and_confirmed_absence"] : []),
11
12
  "pending_local_review_interactions",
12
13
  "wallet_operation_availability_separate_from_stored_transaction_facts"
13
14
  ],
@@ -24,9 +25,11 @@ export function interactionStatusUserAnswerUse(hasWalletObservation = false, has
24
25
  "complete_wallet_history",
25
26
  "profit_or_pnl"
26
27
  ],
27
- preconditionFields: ["walletAvailability", "assetReadAccount", ...(hasConnectionConflict ? ["connectionConflict"] : [])],
28
+ preconditionFields: ["walletAvailability", "assetReadAccount", ...(hasConnectionConflict ? ["connectionConflict"] : []),
29
+ ...(reconciliationRequired ? ["connectionReconciliationRequired"] : [])],
28
30
  answerFields: ["activeAccount", "connections", "assetReadAccount", "pendingWalletConnections", "pendingReviewSessions", "walletAvailability",
29
31
  ...(hasConnectionConflict ? ["connectionConflict"] : []),
32
+ "manageableConnectionIds", ...(reconciliationRequired ? ["connectionReconciliationRequired"] : []),
30
33
  ...(hasWalletObservation ? ["walletObservation"] : [])],
31
34
  diagnosticOnlyFields: [
32
35
  "pendingWalletConnections.truncated",
@@ -66,7 +69,8 @@ export function executionResultUserAnswerUse(fields = {}) {
66
69
  "profit_or_pnl"
67
70
  ],
68
71
  answerFields: [
69
- "walletAvailability", "progress", ...(fields.hasConnectionConflict ? ["connectionConflict"] : []), ...(hasWaitOutcome ? ["waitOutcome"] : []),
72
+ "walletAvailability", "progress", ...(fields.hasConnectionConflict ? ["connectionConflict"] : []),
73
+ ...(fields.reconciliationRequired ? ["connectionReconciliationRequired"] : []), ...(hasWaitOutcome ? ["waitOutcome"] : []),
70
74
  "reviewSessionId",
71
75
  "status",
72
76
  "pollingStatus",
@@ -91,7 +95,7 @@ export function executionResultUserAnswerUse(fields = {}) {
91
95
  }
92
96
  };
93
97
  }
94
- export function reviewStatusUserAnswerUse(hasReviewState, hasAdapterLifecycle = false, hasHumanReadableReview = false, hasSimulation = false, hasConnectionConflict = false) {
98
+ export function reviewStatusUserAnswerUse(hasReviewState, hasAdapterLifecycle = false, hasHumanReadableReview = false, hasSimulation = false, hasConnectionConflict = false, reconciliationRequired = false) {
95
99
  return {
96
100
  canAnswer: [
97
101
  "current_local_review_session_status",
@@ -115,7 +119,8 @@ export function reviewStatusUserAnswerUse(hasReviewState, hasAdapterLifecycle =
115
119
  "profit_or_pnl"
116
120
  ],
117
121
  answerFields: [
118
- "walletAvailability", "progress", ...(hasConnectionConflict ? ["connectionConflict"] : []), "reviewSessionId",
122
+ "walletAvailability", "progress", ...(hasConnectionConflict ? ["connectionConflict"] : []),
123
+ ...(reconciliationRequired ? ["connectionReconciliationRequired"] : []), "reviewSessionId",
119
124
  "status",
120
125
  "pollingStatus",
121
126
  "statusCategory",
@@ -50,11 +50,11 @@ export const SERVER_INSTRUCTIONS = [
50
50
  "For USD-denominated payment coverage, balance total, or shortfall questions, call read.get_server_status, then read.list_settlement_asset_groups, then read.preview_intent_evidence. Use toolAvailability.requiredToolsAvailable and responseSummary.doNotCallQuoteToolsForThisQuestion; do not call wallet inventory or quote tools unless user asks a separate inventory or conversion question. For parity, use read.summarize_settlement_asset_group_parity. If toolAvailability cannot be used, say the current MCP server build cannot support the answer.",
51
51
  "The active account context is read-only, not login, custody or signing permission. Implicit asset reads require session.get_interaction_status.assetReadAccount: available. Otherwise ask for an address only if the tool accepts account; connected-only tools require connection/account selection or wallet recovery.",
52
52
  "Unsupported: other chains, autonomous trading, alerts, investment advice, arbitrary Move/package calls, P&L/tax.",
53
- "One wallet connection at a time; guidance: suimcp://docs/agent-behavior."
53
+ "One wallet connection at a time; docs: suimcp://docs/agent-behavior, suimcp://docs/mcp-tools."
54
54
  ].join("\n");
55
55
  export const IMPLEMENTED_TOOLS = [
56
56
  TOOL_NAMES.uiOpenAccount, TOOL_NAMES.uiOpenReceipt, TOOL_NAMES.uiOpenChart,
57
- TOOL_NAMES.uiReadCard, TOOL_NAMES.uiSubmitCard, TOOL_NAMES.uiActCard,
57
+ TOOL_NAMES.uiReadCard, TOOL_NAMES.uiSubmitCard, TOOL_NAMES.uiActCard, TOOL_NAMES.uiReadSettingsFile,
58
58
  TOOL_NAMES.readGetServerStatus,
59
59
  TOOL_NAMES.readListSupportedProtocols,
60
60
  TOOL_NAMES.readListDeepbookPools,
@@ -86,7 +86,6 @@ export const IMPLEMENTED_TOOLS = [
86
86
  TOOL_NAMES.actionPrepareSuiActionReview,
87
87
  TOOL_NAMES.actionPrepareExternalProposalReview,
88
88
  TOOL_NAMES.accountGetActiveAccount,
89
- TOOL_NAMES.accountClearActiveAccount,
90
89
  TOOL_NAMES.sessionCreateWalletConnection,
91
90
  TOOL_NAMES.sessionGetWalletConnection,
92
91
  TOOL_NAMES.sessionWaitWalletConnection,
@@ -127,5 +126,5 @@ export const SERVER_LIMITATIONS = [
127
126
  `DeepBook orderbook, mid-price, and quote outputs are ${DEEPBOOK_SOURCE_OWNER_RUNTIME_WORDING.pinnedSdkSnapshots} at fetchedAt; treat them as stale if the user delays.`,
128
127
  `DeepBook USDC price history and price-at-time reads use ${DEEPBOOK_SOURCE_OWNER_RUNTIME_WORDING.officialIndexerCandleData} for the requested official interval. They are external candle evidence only, not live quotes, historical mid prices, global market prices, fiat USD values, USDC/USD peg guarantees, route recommendations, transaction-building inputs, signing readiness, P&L, cost basis, user-account transaction history, or user-account balance history. Sui MCP does not independently recompute those candle values from chain history for the response.`,
129
128
  `ui.open_chart displays ${DEEPBOOK_SOURCE_OWNER_RUNTIME_WORDING.officialIndexerUsdcDenominatedCandles} in an internal read-only card. ui.open_account displays a public-address asset snapshot; ui.open_receipt displays the executed transaction graph when bound UI metadata is available, followed by on-chain facts and a Details slide. Card inputs end after backend admission, expiry or invalidation; chat navigation and frame recreation do not end valid unsubmitted input. Read the same DB state after a lost reply rather than resubmitting. Saved results do not authorize a new query or transaction. These cards are not live feeds, trading interfaces, route recommendations, fiat USD value, P&L, tax, cost basis, signing data, or signing readiness.`,
130
- "Settings mutations happen through local settings pages on the review server. MCP settings tools create settings sessions or read current settings only."
129
+ "Settings uses an internal card. MCP settings tools create the card or read current settings; only the user's scoped card confirmation applies local changes or restarts the connection service. Backup bytes stay in app-only file transfer. No external Settings URL exists."
131
130
  ];
@@ -5,6 +5,7 @@ export const TOOL_NAMES = {
5
5
  uiReadCard: "ui.read_card",
6
6
  uiSubmitCard: "ui.submit_card",
7
7
  uiActCard: "ui.act_card",
8
+ uiReadSettingsFile: "ui.read_settings_file",
8
9
  sessionOpenReviewManagement: "session.open_review_management",
9
10
  readGetServerStatus: "read.get_server_status",
10
11
  readListSupportedProtocols: "read.list_supported_protocols",
@@ -43,8 +44,7 @@ export const TOOL_NAMES = {
43
44
  sessionGetReviewStatus: "session.get_review_status",
44
45
  sessionGetExecutionResult: "session.get_execution_result",
45
46
  sessionWaitExecutionResult: "session.wait_execution_result",
46
- accountGetActiveAccount: "account.get_active_account",
47
- accountClearActiveAccount: "account.clear_active_account"
47
+ accountGetActiveAccount: "account.get_active_account"
48
48
  };
49
49
  export function assertValidToolName(name) {
50
50
  if (!/^[A-Za-z0-9_.-]{1,128}$/.test(name)) {
@@ -1,8 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { activeAccountResponse, activeAccountResponseSchema } from "../../activeAccountResponse.js";
3
- import { registerSuiMcpTool } from "../../registerTool.js";
4
3
  import { okToolResult } from "../../result.js";
5
- import { noParamsInputSchema, successOutputSchema } from "../../schemas.js";
4
+ import { noParamsInputSchema } from "../../schemas.js";
6
5
  import { activityStoreToolError } from "../../toolErrors.js";
7
6
  import { TOOL_NAMES } from "../../toolNames.js";
8
7
  export function registerAccountTools(server, deps) {
@@ -24,21 +23,4 @@ export function registerAccountTools(server, deps) {
24
23
  return activityStoreToolError(error, deps.logger);
25
24
  }
26
25
  });
27
- registerSuiMcpTool(server, TOOL_NAMES.accountClearActiveAccount, {
28
- title: "Clear active account read context",
29
- description: "Remove the active read context. Does not disconnect a wallet or revoke onchain permission.",
30
- inputSchema: noParamsInputSchema,
31
- outputSchema: successOutputSchema({
32
- status: z.literal("cleared")
33
- }),
34
- annotations: { readOnlyHint: false, openWorldHint: false }
35
- }, async () => {
36
- try {
37
- await deps.activityStore.clearActiveAccount();
38
- return okToolResult({ status: "cleared" });
39
- }
40
- catch (error) {
41
- return activityStoreToolError(error, deps.logger);
42
- }
43
- });
44
26
  }
@@ -1,4 +1,4 @@
1
- import { CONNECTION_CONFLICT_MESSAGE } from "../../../core/session/walletConnection.js";
1
+ import { CONNECTION_CONFLICT_MESSAGE, CONNECTION_RECONCILIATION_MESSAGE } from "../../../core/session/walletConnection.js";
2
2
  import { ReadServiceCacheError, ReadServiceInputError } from "../../../core/read/readService.js";
3
3
  import { parseSuiAddress } from "../../../core/suiAddress.js";
4
4
  import { errorToolResult } from "../../result.js";
@@ -41,7 +41,7 @@ export async function resolveExplicitOrActiveAccount(input, deps) {
41
41
  };
42
42
  }
43
43
  try {
44
- const context = deps.workflow?.readConnectionContext();
44
+ const context = deps.workflow?.reconcileConnectionContext();
45
45
  if (context?.assetReadAccount.status === "available")
46
46
  return { status: "ok", account: context.assetReadAccount.account };
47
47
  const availability = context?.walletAvailability ?? walletUnavailable("initialization_failed");
@@ -51,13 +51,14 @@ export async function resolveExplicitOrActiveAccount(input, deps) {
51
51
  details: { ...(availability.status === "unavailable" ? { reason: availability.reason } : {}), message: availability.message, walletAvailability: availability } }) };
52
52
  return { status: "error", result: errorToolResult({ kind: active ? "input_invalid" : "active_account_not_set",
53
53
  details: { reason: "connected_account_required",
54
- message: context?.connectionConflict ? CONNECTION_CONFLICT_MESSAGE : "This tool requires a selected account with a usable wallet connection. An address alone cannot be used.",
54
+ message: context?.connectionReconciliationRequired ? CONNECTION_RECONCILIATION_MESSAGE : context?.connectionConflict ? CONNECTION_CONFLICT_MESSAGE : "This tool requires a selected account with a usable wallet connection. An address alone cannot be used.",
55
55
  ...(context?.connectionConflict ? { connectionConflict: context.connectionConflict } : {}),
56
+ ...(context?.connectionReconciliationRequired ? { connectionReconciliationRequired: true } : {}),
56
57
  followUp: { tool: TOOL_NAMES.sessionGetInteractionStatus,
57
58
  answerFields: ["walletAvailability", "connections", "pendingWalletConnections", "assetReadAccount"],
58
59
  reason: `Check current connections and pending operations. Use pendingWalletConnections.items[].cardId with ${TOOL_NAMES.sessionGetWalletConnection} or ${TOOL_NAMES.sessionWaitWalletConnection}. If no operation is pending and the user requests connection or account selection, open ${TOOL_NAMES.sessionCreateWalletConnection}. Only the user may act in that card.` } } }) };
59
60
  }
60
- const message = context?.connectionConflict ? CONNECTION_CONFLICT_MESSAGE + " Provide a Sui address in chat for this public read." : availability.status !== "available"
61
+ const message = context?.connectionReconciliationRequired ? CONNECTION_RECONCILIATION_MESSAGE + " Provide a Sui address in chat for this public read." : context?.connectionConflict ? CONNECTION_CONFLICT_MESSAGE + " Provide a Sui address in chat for this public read." : availability.status !== "available"
61
62
  ? "The wallet connection cannot be checked. Please provide a Sui address in chat to view its assets."
62
63
  : "No connected wallet is available for the selected account. Please provide a Sui address in chat.";
63
64
  return { status: "address_required", message, result: errorToolResult({ kind: "input_invalid",
@@ -20,7 +20,7 @@ export function registerExecutionResultTools(server, deps) {
20
20
  if (!state)
21
21
  return errorToolResult({ kind: "session_not_found", details: { reviewSessionId } });
22
22
  const request = state.request;
23
- return okToolResult({ ...reviewStatusResponse(state), userAnswerUse: executionResultUserAnswerUse({ hasConnectionConflict: !!state.connectionConflict, hasRequest: !!request, hasExecutionResult: !!request?.execution }) });
23
+ return okToolResult({ ...reviewStatusResponse(state), userAnswerUse: executionResultUserAnswerUse({ hasConnectionConflict: !!state.connectionConflict, reconciliationRequired: !!state.connectionReconciliationRequired, hasRequest: !!request, hasExecutionResult: !!request?.execution }) });
24
24
  }
25
25
  catch (error) {
26
26
  return sessionStoreToolError(error, deps.logger);
@@ -35,7 +35,7 @@ export function registerExecutionResultTools(server, deps) {
35
35
  try {
36
36
  const result = await waitForExecutionResult((id) => readCurrentReview(deps, id), reviewSessionId, { timeoutMs, signal: extra.signal });
37
37
  return okToolResult({ waitOutcome: result.waitOutcome, ...reviewStatusResponse(result),
38
- userAnswerUse: executionResultUserAnswerUse({ hasConnectionConflict: !!result.connectionConflict, hasRequest: !!result.request, hasExecutionResult: !!result.request?.execution, hasWaitOutcome: true }) });
38
+ userAnswerUse: executionResultUserAnswerUse({ hasConnectionConflict: !!result.connectionConflict, reconciliationRequired: !!result.connectionReconciliationRequired, hasRequest: !!result.request, hasExecutionResult: !!result.request?.execution, hasWaitOutcome: true }) });
39
39
  }
40
40
  catch (error) {
41
41
  return sessionStoreToolError(error, deps.logger);
@@ -15,6 +15,7 @@ export function executionStatusCategorySchema() { return z.enum(EXECUTION_STATUS
15
15
  export const timeoutInputSchema = z.number().int().min(1).max(MAX_WAIT_TIMEOUT_MS).default(DEFAULT_WAIT_TIMEOUT_MS).optional();
16
16
  export function waitExecutionInputSchema() { return { reviewSessionId: z.string().min(1), timeoutMs: timeoutInputSchema }; }
17
17
  export const reviewStatusResponseShape = {
18
+ connectionReconciliationRequired: z.literal(true).optional(),
18
19
  connectionConflict: connectionConflictSchema.optional(), walletAvailability: walletAvailabilitySchema, walletObservation: walletObservationSchema.optional(), progress: workflowProgressSchema,
19
20
  reviewSessionId: z.string(), status: internalSessionStatusSchema, reviewRevision: z.number().int().nonnegative(),
20
21
  account: z.string().optional(), plans: z.array(actionPlanSchema), reviewState: reviewStateOutputSchema.optional(),
@@ -18,6 +18,7 @@ export function registerSessionStatusTools(server, deps) {
18
18
  outputSchema: successOutputSchema({ walletAvailability: walletAvailabilitySchema, walletObservation: walletObservationSchema.optional(), activeAccount: activeAccountResponseSchema,
19
19
  connections: z.array(connectionViewSchema), assetReadAccount: assetReadAccountSchema,
20
20
  connectionConflict: connectionConflictSchema.optional(),
21
+ manageableConnectionIds: z.array(z.string().min(1)), connectionReconciliationRequired: z.literal(true).optional(),
21
22
  pendingWalletConnections: z.object({ limit: z.number().int().positive(), truncated: z.boolean(),
22
23
  items: z.array(z.object({ cardId: z.string(), connectionId: z.string().optional(),
23
24
  status: pendingConnectionStatusSchema, progress: workflowProgressSchema, lastActivityAt: z.string() })) }),
@@ -35,14 +36,14 @@ export function registerSessionStatusTools(server, deps) {
35
36
  try {
36
37
  // Connection reconciliation can invalidate reviews and interrupt requests.
37
38
  // Complete that maintenance before projecting its dependent review facts.
38
- const context = deps.workflow?.readConnectionContext() ?? { connections: [],
39
+ const context = deps.workflow?.reconcileConnectionContext() ?? { connections: [], manageableConnectionIds: [],
39
40
  assetReadAccount: { status: "address_required" }, walletAvailability: walletUnavailable("initialization_failed") };
40
41
  const connections = deps.workflow?.pendingConnections() ?? [];
41
42
  const states = await Promise.all(deps.sessions.reviewSessionIds().map((id) => readCurrentReview(deps, id)));
42
43
  const reviews = states.flatMap((state) => state && isReviewInteractionPending(state) ? [reviewStatusResponse(state)] : []);
43
44
  return okToolResult({ activeAccount: activeAccountResponse(active), pendingWalletConnections: latest(connections),
44
45
  pendingReviewSessions: latest(reviews), ...context,
45
- userAnswerUse: interactionStatusUserAnswerUse("walletObservation" in context && context.walletObservation !== undefined, "connectionConflict" in context && !!context.connectionConflict) });
46
+ userAnswerUse: interactionStatusUserAnswerUse("walletObservation" in context && context.walletObservation !== undefined, "connectionConflict" in context && !!context.connectionConflict, "connectionReconciliationRequired" in context && !!context.connectionReconciliationRequired) });
46
47
  }
47
48
  catch (error) {
48
49
  return sessionStoreToolError(error, deps.logger);
@@ -60,7 +61,7 @@ export function registerSessionStatusTools(server, deps) {
60
61
  return errorToolResult({ kind: "session_not_found", details: { reviewSessionId } });
61
62
  const session = state.session;
62
63
  return okToolResult({ ...reviewStatusResponse(state),
63
- userAnswerUse: reviewStatusUserAnswerUse(!!session.reviewState, !!session.reviewState?.adapterLifecycle, !!session.reviewState?.humanReadableReview, !!session.reviewState?.simulation, !!state.connectionConflict) });
64
+ userAnswerUse: reviewStatusUserAnswerUse(!!session.reviewState, !!session.reviewState?.adapterLifecycle, !!session.reviewState?.humanReadableReview, !!session.reviewState?.simulation, !!state.connectionConflict, !!state.connectionReconciliationRequired) });
64
65
  }
65
66
  catch (error) {
66
67
  return sessionStoreToolError(error, deps.logger);
@@ -1,10 +1,13 @@
1
1
  import { z } from "zod";
2
2
  import { SETTINGS_APPLIES_AFTER_RESTART } from "../../../core/preferences/preferencesStore.js";
3
3
  import { registerSuiMcpTool } from "../../registerTool.js";
4
- import { errorToolResult, okToolResult } from "../../result.js";
4
+ import { okToolResult } from "../../result.js";
5
5
  import { noParamsInputSchema, successOutputSchema } from "../../schemas.js";
6
6
  import { localSettingsToolError, sessionStoreToolError } from "../../toolErrors.js";
7
7
  import { TOOL_NAMES } from "../../toolNames.js";
8
+ import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
9
+ import { cardMetadata, createWorkflowCard } from "../../../mcp-ui/tools.js";
10
+ import { cardSnapshotSchema } from "../../../mcp-ui/contracts.js";
8
11
  const endpointSettingSchema = z.object({
9
12
  storedValue: z.string(),
10
13
  effectiveValue: z.string(),
@@ -13,34 +16,11 @@ const endpointSettingSchema = z.object({
13
16
  appliesAfter: z.literal(SETTINGS_APPLIES_AFTER_RESTART).optional()
14
17
  });
15
18
  export function registerSettingsTools(server, deps) {
16
- server.registerTool(TOOL_NAMES.settingsCreateLocalSettingsSession, {
17
- title: "Create local settings session",
18
- description: "Create a local settings page session.",
19
- inputSchema: noParamsInputSchema,
20
- outputSchema: successOutputSchema({
21
- settingsSessionId: z.string(),
22
- settingsUrl: z.string(),
23
- status: z.literal("created"),
24
- openTarget: z.literal("system_browser"),
25
- accessScope: z.literal("same_machine_loopback"),
26
- expiresAt: z.string(),
27
- lastActivityAt: z.string()
28
- }),
29
- annotations: { readOnlyHint: false, openWorldHint: false }
30
- }, async () => {
19
+ registerAppTool(server, TOOL_NAMES.settingsCreateLocalSettingsSession, { title: "Settings", description: "Open a local settings card.", inputSchema: noParamsInputSchema,
20
+ outputSchema: successOutputSchema(cardSnapshotSchema.shape), _meta: cardMetadata("settings"),
21
+ annotations: { readOnlyHint: false, openWorldHint: false } }, async () => {
31
22
  try {
32
- if (!deps.reviewBaseUrl)
33
- return errorToolResult({ kind: "ui_unavailable", details: { reason: "Settings UI is unavailable in this runtime." } });
34
- const created = await deps.sessions.createSettingsSession();
35
- return okToolResult({
36
- settingsSessionId: created.session.id,
37
- settingsUrl: `${deps.reviewBaseUrl}/settings/${created.session.id}#${created.token}`,
38
- status: "created",
39
- openTarget: "system_browser",
40
- accessScope: "same_machine_loopback",
41
- expiresAt: created.session.expiresAt,
42
- lastActivityAt: created.session.lastActivityAt
43
- });
23
+ return await createWorkflowCard(server, deps, "settings", {});
44
24
  }
45
25
  catch (error) {
46
26
  return sessionStoreToolError(error, deps.logger);