@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
@@ -7,11 +7,13 @@ export const CARD_RESOURCE_URIS = {
7
7
  receipt: "ui://sui-mcp/receipt.html",
8
8
  chart: "ui://sui-mcp/chart.html",
9
9
  connect: "ui://sui-mcp/connect.html",
10
- review: "ui://sui-mcp/review.html"
10
+ review: "ui://sui-mcp/review.html",
11
+ settings: "ui://sui-mcp/settings.html"
11
12
  };
12
13
  export const CARD_METADATA_KEY = "sui-mcp/card";
13
14
  export const CARD_DISPLAY_METADATA_KEY = "sui-mcp/receipt-display";
14
15
  export const WALLET_DISPLAY_METADATA_KEY = "sui-mcp/wallet-display";
16
+ export const SETTINGS_FILE_METADATA_KEY = "sui-mcp/settings-file";
15
17
  export const CARD_RESOURCE_PREFIX = "suimcp://cards/";
16
18
  export const cardInputRequiredSchema = z.discriminatedUnion("kind", [
17
19
  z.object({ kind: z.literal("account"), status: z.literal("input_required"), field: z.literal("account"), message: z.string().min(1) }).strict(),
@@ -25,7 +27,11 @@ export const cardReceiptDisplaySchema = receiptDisplaySchema.extend({
25
27
  cardId: z.string().min(1), revision: z.number().int().nonnegative(), attemptId: z.string().min(1).optional()
26
28
  });
27
29
  export const cardWalletDisplaySchema = walletDisplaySchema.extend({ cardId: z.string().min(1), revision: z.number().int().nonnegative() });
30
+ export const settingsFileDisplaySchema = z.object({ attemptId: z.string().uuid().optional(), cardId: z.string().min(1), operationId: z.string().uuid(), fileId: z.string().uuid(),
31
+ offset: z.number().int().nonnegative().safe(), totalSize: z.number().int().nonnegative().safe(), sha256: z.string().regex(/^[a-f0-9]{64}$/),
32
+ chunkBase64: z.string() }).strict();
28
33
  export const CARD_TOOLS = {
29
34
  account: TOOL_NAMES.uiOpenAccount, receipt: TOOL_NAMES.uiOpenReceipt, chart: TOOL_NAMES.uiOpenChart,
30
- read: TOOL_NAMES.uiReadCard, submit: TOOL_NAMES.uiSubmitCard, act: TOOL_NAMES.uiActCard
35
+ read: TOOL_NAMES.uiReadCard, submit: TOOL_NAMES.uiSubmitCard, act: TOOL_NAMES.uiActCard,
36
+ readSettingsFile: TOOL_NAMES.uiReadSettingsFile
31
37
  };
@@ -2,7 +2,7 @@ import { CardStore } from "../core/session/cardSessionStore.js";
2
2
  import { accountInputSchema, receiptInputSchema, chartInputSchema } from "../core/read/readCardInputs.js";
3
3
  export function createReadCardStore(options) {
4
4
  return new CardStore({
5
- records: options.records, ownerId: options.ownerId, assertCurrent: options.assertCurrent, now: options.now, logger: options.logger, workflow: options.workflow,
5
+ records: options.records, ownerId: options.ownerId, assertCurrent: options.assertCurrent, now: options.now, logger: options.logger, workflow: options.workflow, settings: options.settings,
6
6
  prepare: async (kind) => {
7
7
  if (kind !== "chart")
8
8
  return { status: "ready" };
@@ -1,4 +1,6 @@
1
1
  import { workflowActionSchema } from "../core/session/workflowView.js";
2
+ import { settingsActionSchema } from "../core/session/settingsOperation.js";
3
+ import { MAX_JSON_BODY_BYTES, settingsJsonSize, settingsRawChunkBudget } from "../core/session/settingsFileProtocol.js";
2
4
  import { sessionDomainToolError } from "../mcp/toolErrors.js";
3
5
  import { accountInputSchema, receiptInputSchema, chartOpenInputSchema } from "../core/read/readCardInputs.js";
4
6
  import { readFile } from "node:fs/promises";
@@ -8,8 +10,8 @@ import { z } from "zod";
8
10
  import { okToolResult, errorToolResult } from "../mcp/result.js";
9
11
  import { CardError } from "../core/session/cardSessionStore.js";
10
12
  import { resolveExplicitOrActiveAccount } from "../mcp/tools/read/readToolHelpers.js";
11
- import { CARD_METADATA_KEY, CARD_DISPLAY_METADATA_KEY, WALLET_DISPLAY_METADATA_KEY, CARD_RESOURCE_PREFIX, CARD_RESOURCE_URIS, CARD_TOOLS, cardReceiptDisplaySchema, cardWalletDisplaySchema, cardReferenceSchema, cardSubmissionSchema, cardInputRequiredSchema } from "./contracts.js";
12
- const cardActionSchema = cardReferenceSchema.extend({ revision: z.number().int().nonnegative(), input: workflowActionSchema }).strict();
13
+ import { CARD_METADATA_KEY, CARD_DISPLAY_METADATA_KEY, WALLET_DISPLAY_METADATA_KEY, SETTINGS_FILE_METADATA_KEY, CARD_RESOURCE_PREFIX, CARD_RESOURCE_URIS, CARD_TOOLS, cardReceiptDisplaySchema, cardWalletDisplaySchema, cardReferenceSchema, cardSubmissionSchema, cardInputRequiredSchema } from "./contracts.js";
14
+ const cardActionSchema = cardReferenceSchema.extend({ revision: z.number().int().nonnegative().safe(), input: z.union([workflowActionSchema, settingsActionSchema]) }).strict();
13
15
  export const cardMetadata = (kind) => ({
14
16
  ui: { resourceUri: CARD_RESOURCE_URIS[kind], visibility: ["model"] },
15
17
  "openai/outputTemplate": CARD_RESOURCE_URIS[kind]
@@ -131,4 +133,33 @@ export function registerReadCards(server, deps) {
131
133
  }
132
134
  });
133
135
  }
136
+ const fileInput = cardReferenceSchema.extend({ attemptId: z.string().uuid().optional(), operationId: z.string().uuid(), fileId: z.string().uuid(), offset: z.number().int().nonnegative().safe(),
137
+ limit: z.number().int().positive().safe().optional() }).strict();
138
+ registerAppTool(server, CARD_TOOLS.readSettingsFile, { description: "Read a verified backup file for its current download attempt.", inputSchema: fileInput,
139
+ _meta: { ui: { visibility: ["app"] } }, annotations: { readOnlyHint: true, openWorldHint: false }
140
+ }, async (input, extra) => {
141
+ try {
142
+ if (!deps.cards)
143
+ throw new CardError("Card service is unavailable.");
144
+ // Measure the actual response shape, including its text fallback and UI
145
+ // metadata. Reserve maximum safe-integer digit widths for size fields.
146
+ const base = okToolResult({ status: "read", fileId: input.fileId, offset: input.offset, rangeSize: Number.MAX_SAFE_INTEGER });
147
+ const metadata = { ...(input.attemptId ? { attemptId: input.attemptId } : {}), cardId: input.cardId, operationId: input.operationId, fileId: input.fileId, offset: input.offset, totalSize: Number.MAX_SAFE_INTEGER,
148
+ sha256: "0".repeat(64), chunkBase64: "" };
149
+ base._meta = { [SETTINGS_FILE_METADATA_KEY]: metadata };
150
+ const budget = settingsRawChunkBudget({ jsonrpc: "2.0", id: extra.requestId, result: base });
151
+ if (!budget)
152
+ throw new CardError("The backup response cannot fit the app's message envelope.");
153
+ const read = await deps.cards.store.readSettingsFile(input, Math.min(input.limit ?? budget, budget));
154
+ const rangeSize = Buffer.from(read.chunkBase64, "base64").length;
155
+ const result = okToolResult({ status: "read", fileId: input.fileId, offset: input.offset, rangeSize });
156
+ result._meta = { [SETTINGS_FILE_METADATA_KEY]: { ...metadata, totalSize: read.file.totalSize, sha256: read.file.sha256, chunkBase64: read.chunkBase64 } };
157
+ if (settingsJsonSize({ jsonrpc: "2.0", id: extra.requestId, result }) > MAX_JSON_BODY_BYTES)
158
+ throw new CardError("The backup response exceeds its message limit.");
159
+ return result;
160
+ }
161
+ catch {
162
+ return errorToolResult({ kind: "input_invalid", details: { reason: "This backup file could not be read or verified." } });
163
+ }
164
+ });
134
165
  }
@@ -1,10 +1,8 @@
1
- import { createServer } from "node:http";
2
1
  import { validateHostOrigin } from "./middleware/hostOrigin.js";
3
- import { defaultReviewAssetsDir, serveReviewAsset } from "./assets.js";
4
- import { settingsHtml } from "./html.js";
5
- import { HttpError, sendHtml, sendJson } from "./http.js";
2
+ import { sendJson } from "../runtime/shared/http.js";
6
3
  import { ALLOWED_HOSTNAMES } from "./reviewServerPolicy.js";
7
- import { routeSettingsApi } from "./settingsApi.js";
4
+ // The shared runtime serves authenticated private MCP/control routes itself.
5
+ // There are no public browser pages, Settings APIs or static-asset fallbacks.
8
6
  export function createReviewRequestHandler(options) {
9
7
  return async (request, response) => {
10
8
  try {
@@ -18,55 +16,11 @@ export function createReviewRequestHandler(options) {
18
16
  sendJson(response, 400, { error: "token_query_not_supported" });
19
17
  return;
20
18
  }
21
- const settings = /^\/settings\/([^/]+)$/.exec(url.pathname);
22
- if (request.method === "GET" && settings?.[1]) {
23
- sendHtml(response, settingsHtml(settings[1]), { "content-security-policy": [
24
- "default-src 'none'", "base-uri 'none'", "connect-src 'self'", "script-src 'self'",
25
- "style-src 'self'", "img-src 'self' data:", "form-action 'none'"
26
- ].join("; ") });
27
- return;
28
- }
29
- const asset = /^\/review-assets\/(.+)$/.exec(url.pathname);
30
- if (request.method === "GET" && asset?.[1]) {
31
- await serveReviewAsset(response, options.reviewAssetsDir ?? defaultReviewAssetsDir(), asset[1]);
32
- return;
33
- }
34
- const route = /^\/api\/settings\/([^/]+)(?:\/(.*))?$/.exec(url.pathname);
35
- if (route?.[1]) {
36
- const routes = {
37
- "": "status", "clear-active-account": "clearActiveAccount", "sui-grpc-url": "setSuiGrpcUrl",
38
- "sui-grpc-url/restore-default": "restoreDefaultSuiGrpcUrl", "sui-graphql-url": "setSuiGraphqlUrl",
39
- "sui-graphql-url/restore-default": "restoreDefaultSuiGraphqlUrl", "local-data/export": "exportLocalData",
40
- "local-data/import/preview": "previewImport", "local-data/import": "importLocalData", "local-data/reset": "resetLocalData"
41
- };
42
- const match = routes[route[2] ?? ""];
43
- if (match) {
44
- await routeSettingsApi(request, response, options, url, { [match]: route[1] });
45
- return;
46
- }
47
- }
48
19
  sendJson(response, 404, { error: "not_found" });
49
20
  }
50
- catch (error) {
51
- if (error instanceof HttpError) {
52
- sendJson(response, error.status, { error: error.code });
53
- return;
54
- }
55
- options.logger.error("settings request failed", { stage: "settings_http" });
21
+ catch {
22
+ options.logger.error("HTTP routing failed", { stage: "http_routing" });
56
23
  sendJson(response, 500, { error: "internal_error" });
57
24
  }
58
25
  };
59
26
  }
60
- export function createReviewHttpServer(options) {
61
- const handler = createReviewRequestHandler(options);
62
- const server = createServer((request, response) => { void handler(request, response); });
63
- return {
64
- start(port) {
65
- return new Promise((resolve, reject) => {
66
- server.once("error", reject);
67
- server.listen(port, options.host, () => resolve({ host: options.host, port: server.address().port,
68
- close: () => new Promise((done, failed) => { server.close((error) => error ? failed(error) : done()); }) }));
69
- });
70
- }
71
- };
72
- }
@@ -5,6 +5,8 @@ import { dirname } from "node:path";
5
5
  import { WalletSdkProcess } from "./walletSdkProcess.js";
6
6
  import { WALLETCONNECT_PROJECT_ID } from "./walletConnectConfig.js";
7
7
  import { WalletWorkflow } from "../core/session/walletWorkflow.js";
8
+ import { SettingsWorkflow } from "../core/session/settingsWorkflow.js";
9
+ import { SettingsFileStore } from "../core/session/settingsFileStore.js";
8
10
  import { randomUUID } from "node:crypto";
9
11
  import { SqliteActivityStore } from "../core/activity/sqliteActivityStore.js";
10
12
  import { validateSupportedAdapterLifecycle } from "../adapters/adapterLifecycleValidators.js";
@@ -33,6 +35,7 @@ export async function createRuntimeApplication(bootConfig, logger, ownerId = ran
33
35
  let activityStore;
34
36
  let cards;
35
37
  let workflow;
38
+ let settings;
36
39
  let chart;
37
40
  try {
38
41
  const store = new SqliteActivityStore({
@@ -68,10 +71,10 @@ export async function createRuntimeApplication(bootConfig, logger, ownerId = ran
68
71
  bootSuiGrpcUrl: config.suiGrpcUrl,
69
72
  bootSuiGraphqlUrl: config.suiGraphqlUrl
70
73
  });
71
- const workflowRecords = store.createWalletWorkflowStore(ownerId);
74
+ const workflowRecords = store.createWalletWorkflowStore(ownerId, () => new Date(), () => walletRuntime?.snapshot());
72
75
  const localData = store.createLocalDataService({
73
76
  advanceRequestDeadlines: (now) => workflowRecords.advanceRequestDeadlines(now),
74
- onDataReplaced: () => { access.dataReplaced(); workflow?.dataReplaced(); chart?.clearCache(); },
77
+ onDataReplaced: () => { access.dataReplaced(); workflow?.dataReplaced(); settings?.dataReplaced(); chart?.clearCache(); },
75
78
  suiGrpcUrl: DEFAULT_SUI_GRPC_URL,
76
79
  suiGraphqlUrl: DEFAULT_SUI_GRAPHQL_URL,
77
80
  verifySuiGrpcUrl: async (url) => {
@@ -95,8 +98,7 @@ export async function createRuntimeApplication(bootConfig, logger, ownerId = ran
95
98
  validateAdapterLifecycle: validateSupportedAdapterLifecycle,
96
99
  sessions: store.createSessionRecordStore(),
97
100
  artifacts: store.createPrivateReviewArtifactStore(),
98
- ownerId,
99
- settingsStore: store.createSettingsRecordStore()
101
+ ownerId
100
102
  });
101
103
  const readService = createSuiReadService({
102
104
  client: suiClient,
@@ -118,9 +120,7 @@ export async function createRuntimeApplication(bootConfig, logger, ownerId = ran
118
120
  }, input);
119
121
  const reviewComputationDeps = createRuntimeReviewDependencies({ client: suiClient, chainIdentifier,
120
122
  expectedChainIdentifier: config.expectedChainIdentifier, materialStore: transactionMaterialStore, readService });
121
- const httpHandler = createReviewRequestHandler({ host: config.reviewHost, store: sessions, logger,
122
- activityStore: store, localSettings, localData,
123
- serverInfo: { name: SERVER_NAME, version: SERVER_VERSION, network: SERVER_NETWORK } });
123
+ const httpHandler = createReviewRequestHandler({ logger });
124
124
  const cardRecords = store.createCardRecordStore();
125
125
  let metadata;
126
126
  try {
@@ -141,9 +141,26 @@ export async function createRuntimeApplication(bootConfig, logger, ownerId = ran
141
141
  },
142
142
  submitTransaction: (transaction, signature) => suiClient.core.executeTransaction({ transaction, signatures: [signature] })
143
143
  });
144
+ const settingsRecords = store.createSettingsWorkflowStore(ownerId);
145
+ settings = new SettingsWorkflow({ records: settingsRecords, files: new SettingsFileStore(dirname(bootConfig.activityDatabasePath)),
146
+ activity: store, localData, preferences: preferencesRepository, wallet: workflow, assertCurrent: access.assertCurrent, runMaintenance: (work) => access.run(work),
147
+ resolveEndpoint: (input) => localSettings.resolveEndpointChange(input),
148
+ verifyEndpoint: (input) => localSettings.prepareEndpointChange(input),
149
+ interruptRun: (runId) => workflowRecords.invalidateWalletRun("wallet_service_restarted", new Date(), runId),
150
+ readFacts: async () => settingsRecords.transaction(() => {
151
+ const context = workflow.currentConnectionContext(), account = store.getActiveAccountSync();
152
+ const observedAt = context.walletObservation?.observedAt;
153
+ return { server: { name: SERVER_NAME, version: SERVER_VERSION, network: SERVER_NETWORK },
154
+ localSettings: localSettings.getLocalSettingsSync((key) => preferencesRepository.getLocalSettingSync(key)),
155
+ ...(account ? { readAddress: account.address } : {}),
156
+ assetReadAccount: context.assetReadAccount, walletAvailability: context.walletAvailability,
157
+ ...(context.connectionReconciliationRequired ? { connectionReconciliationRequired: true } : {}),
158
+ ...(observedAt ? { observedAt } : {}), counts: localData.getDataCountsSync() };
159
+ }) });
160
+ settings.recoverOwner();
144
161
  await workflow.start();
145
162
  chart = createDeepbookUsdcChartService({ assertCurrent: access.assertCurrent });
146
- cards = createReadCardStore({ readService, publicChainReceiptReader, chart, records: cardRecords, ownerId, assertCurrent: access.assertCurrent, logger, workflow });
163
+ cards = createReadCardStore({ readService, publicChainReceiptReader, chart, records: cardRecords, ownerId, assertCurrent: access.assertCurrent, logger, workflow, settings });
147
164
  const transactionActivityService = new TransactionActivityService({
148
165
  activityStore: store,
149
166
  source: new GraphqlSuiTransactionActivitySource({
@@ -157,31 +174,41 @@ export async function createRuntimeApplication(bootConfig, logger, ownerId = ran
157
174
  promptSurfaces: ADAPTER_PROMPT_SURFACES,
158
175
  sessions,
159
176
  activityStore: store,
160
- reviewBaseUrl: `http://${config.reviewHost}:${config.reviewPort}`,
161
177
  readService,
162
178
  transactionActivityService,
163
179
  localSettings,
164
180
  logger
165
181
  }));
166
182
  access.ready();
183
+ void access.run(() => settings.cleanupAfterOwnerChange()).catch(() => {
184
+ try {
185
+ logger.error("Temporary settings file cleanup is incomplete", { stage: "settings_file_cleanup" });
186
+ }
187
+ catch { /* Stored cleanup facts remain authoritative. */ }
188
+ });
167
189
  return {
168
190
  handleMcp: (request, response) => access.run(() => mcp.handle(request, response)),
169
191
  handleHttp: (request, response) => access.run(() => httpHandler(request, response)),
170
192
  async close() {
171
193
  workflow?.stop();
172
- access.close();
173
194
  cards?.stop();
174
195
  chart?.clearCache();
175
196
  try {
176
- await walletRuntime.close();
197
+ await settings.close();
177
198
  }
178
199
  finally {
200
+ access.close();
179
201
  try {
180
- await mcp.close();
202
+ await walletRuntime.close();
181
203
  }
182
204
  finally {
183
- store.close();
184
- ownership.close();
205
+ try {
206
+ await mcp.close();
207
+ }
208
+ finally {
209
+ store.close();
210
+ ownership.close();
211
+ }
185
212
  }
186
213
  }
187
214
  }
@@ -33,14 +33,23 @@ export class RuntimeLocalSettingsService {
33
33
  async getLocalSettings() {
34
34
  const stored = await this.options.preferencesRepository.getSuiGrpcUrl();
35
35
  const storedGraphql = await this.options.preferencesRepository.getSuiGraphqlUrl();
36
+ return this.settingsFromStored(stored?.value, storedGraphql?.value);
37
+ }
38
+ getLocalSettingsSync(read) {
39
+ const grpc = read("suiGrpcUrl"), graphql = read("suiGraphqlUrl");
40
+ if (grpc && typeof grpc.value !== "string" || graphql && typeof graphql.value !== "string")
41
+ throw new LocalSettingsError("internal_error", "Stored endpoints are invalid.");
42
+ return this.settingsFromStored(grpc?.value, graphql?.value);
43
+ }
44
+ settingsFromStored(stored, storedGraphql) {
36
45
  const currentStoredView = resolveSuiGrpcUrlView({
37
46
  defaultSuiGrpcUrl: this.defaultSuiGrpcUrl,
38
- storedValue: stored?.value,
47
+ storedValue: stored,
39
48
  envValue: this.env.SUI_GRPC_URL
40
49
  });
41
50
  const currentStoredGraphqlView = resolveSuiGraphqlUrlView({
42
51
  defaultSuiGraphqlUrl: this.defaultSuiGraphqlUrl,
43
- storedValue: storedGraphql?.value,
52
+ storedValue: storedGraphql,
44
53
  envValue: this.env.SUI_GRAPHQL_URL
45
54
  });
46
55
  return {
@@ -48,6 +57,21 @@ export class RuntimeLocalSettingsService {
48
57
  suiGraphqlUrl: withBootEffectiveEndpoint(currentStoredGraphqlView, this.bootSuiGraphqlUrl)
49
58
  };
50
59
  }
60
+ // The same side-effect-free target resolution serves display and final
61
+ // verification. Default URLs belong to this service, never to the View.
62
+ resolveEndpointChange(input) {
63
+ const value = input.kind === "restore_endpoint"
64
+ ? input.endpoint === "grpc" ? this.defaultSuiGrpcUrl : this.defaultSuiGraphqlUrl
65
+ : input.endpoint === "grpc" ? parseEndpointForTool(input.value ?? "") : parseGraphqlEndpointForTool(input.value ?? "");
66
+ return { key: input.endpoint === "grpc" ? "suiGrpcUrl" : "suiGraphqlUrl", value,
67
+ status: input.kind === "restore_endpoint" ? "reset" : "saved" };
68
+ }
69
+ // Verification does not store a value or change this parent's effective URL.
70
+ async prepareEndpointChange(input) {
71
+ const target = this.resolveEndpointChange(input);
72
+ await this.verifyEndpointForTool(target.value, input.endpoint);
73
+ return target;
74
+ }
51
75
  async setSuiGrpcUrl(url) {
52
76
  const normalized = parseEndpointForTool(url);
53
77
  await this.verifyEndpointForTool(normalized, "grpc");
@@ -2,7 +2,7 @@ import { Agent, request as httpRequest } from "node:http";
2
2
  import { connect } from "node:net";
3
3
  import { randomBytes } from "node:crypto";
4
4
  import { Readable } from "node:stream";
5
- import { MAX_JSON_BODY_BYTES } from "../../review-server/http.js";
5
+ import { MAX_JSON_BODY_BYTES } from "./http.js";
6
6
  import { IDENTITY_CHALLENGE_HEADER, IDENTITY_PATH, IDENTITY_TIMEOUT_MS, INTERNAL_MCP_PATH, SERVER_INSTANCE_HEADER, verifyServerIdentity } from "./control.js";
7
7
  // Proof and authenticated request use the same TCP socket. A closed socket is never
8
8
  // transparently replaced: no credential or UI permission can reach a new listener.
@@ -86,7 +86,11 @@ export function createAuthenticatedFetch(port, control, expectedInstanceId) {
86
86
  headers.set("host", url.host);
87
87
  headers.set("authorization", `Bearer ${control.key}`);
88
88
  headers.set(SERVER_INSTANCE_HEADER, channel.identity.instanceId);
89
- headers.set("connection", "close");
89
+ // An early rejection (for example the SDK's body-size guard) must reach
90
+ // the caller while the request is still being written. Server-side
91
+ // Connection: close can race that response with EPIPE. This channel is
92
+ // still destroyed after the response and never reused or replaced.
93
+ headers.set("connection", "keep-alive");
90
94
  const body = init?.body;
91
95
  if (body !== undefined && body !== null && typeof body !== "string") {
92
96
  channel.close();
@@ -2,9 +2,8 @@ import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from
2
2
  import { link, lstat, mkdir, readFile, realpath, unlink, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, join } from "node:path";
4
4
  import { z } from "zod";
5
- // Version 7 uses the Sui MCP resource, metadata and response-field identities.
6
- // Clients must not attach to an owner that serves the previous package contract.
7
- export const INTERNAL_API_VERSION = 7;
5
+ import { SERVER_VERSION } from "../../mcp/serverInfo.js";
6
+ // Peers use the one package release identity, with no separate API counter.
8
7
  export const IDENTITY_PATH = "/__runtime/identity";
9
8
  export const INTERNAL_MCP_PATH = "/__runtime/mcp";
10
9
  export const IDENTITY_CHALLENGE_HEADER = "x-sui-mcp-identity-challenge";
@@ -15,7 +14,7 @@ const secretSchema = z.string().regex(/^[A-Za-z0-9_-]{43}$/);
15
14
  export const serverIdentitySchema = z.object({
16
15
  service: z.literal("sui-mcp"),
17
16
  role: z.literal("shared-server"),
18
- apiVersion: z.literal(INTERNAL_API_VERSION),
17
+ packageVersion: z.literal(SERVER_VERSION),
19
18
  databaseId: z.string().regex(/^[0-9a-f]{64}$/),
20
19
  configurationId: z.string().regex(/^[0-9a-f]{64}$/),
21
20
  instanceId: z.string().uuid(),
@@ -53,12 +52,12 @@ export async function loadControlIdentity(databasePath, configuration) {
53
52
  };
54
53
  }
55
54
  function identityPayload(identity) {
56
- return JSON.stringify([identity.service, identity.role, identity.apiVersion, identity.databaseId,
55
+ return JSON.stringify([identity.service, identity.role, identity.packageVersion, identity.databaseId,
57
56
  identity.configurationId, identity.instanceId, identity.pid, identity.challenge]);
58
57
  }
59
58
  export function createServerIdentity(control, instanceId, challenge) {
60
59
  secretSchema.parse(challenge);
61
- const identity = { service: "sui-mcp", role: "shared-server", apiVersion: INTERNAL_API_VERSION,
60
+ const identity = { service: "sui-mcp", role: "shared-server", packageVersion: SERVER_VERSION,
62
61
  databaseId: control.databaseId, configurationId: control.configurationId,
63
62
  instanceId, pid: process.pid, challenge };
64
63
  return { ...identity, proof: createHmac("sha256", control.key).update(identityPayload(identity)).digest("base64url") };
@@ -0,0 +1,14 @@
1
+ export { MAX_JSON_BODY_BYTES } from "../../core/session/settingsFileProtocol.js";
2
+ export class HttpError extends Error {
3
+ status;
4
+ code;
5
+ constructor(status, code) {
6
+ super(code);
7
+ this.status = status;
8
+ this.code = code;
9
+ }
10
+ }
11
+ export function sendJson(response, status, body) {
12
+ response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
13
+ response.end(JSON.stringify(body));
14
+ }
@@ -2,7 +2,8 @@ import { CARD_TOOLS } from "../../mcp-ui/contracts.js";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
4
  import { isJSONRPCRequest } from "@modelcontextprotocol/sdk/types.js";
5
- import { MAX_JSON_BODY_BYTES, sendJson } from "../../review-server/http.js";
5
+ import { MAX_JSON_BODY_BYTES, sendJson } from "./http.js";
6
+ import { settingsUploadMessageBudget } from "../../core/session/settingsFileProtocol.js";
6
7
  export function createInternalMcpHandler(createServer) {
7
8
  const sessions = new Map();
8
9
  const connections = new Set();
@@ -48,12 +49,16 @@ export function createInternalMcpHandler(createServer) {
48
49
  close: () => transport.close()
49
50
  };
50
51
  transport.onmessage = (message, extra) => {
51
- // Preserve ordinary stdio MCP input contracts. The existing HTTP/UI
52
- // body limit applies to card calls, including their JSON-RPC envelope.
52
+ // The SDK enforces its 4 MiB HTTP body limit before this callback.
53
+ // Card calls additionally have a 64 KiB JSON-RPC envelope budget;
54
+ // ordinary MCP calls do not inherit that smaller card limit.
53
55
  if (isJSONRPCRequest(message) && message.method === "tools/call" &&
54
56
  Object.values(CARD_TOOLS).some((name) => name === message.params?.name) &&
55
57
  Buffer.byteLength(JSON.stringify(message)) > MAX_JSON_BODY_BYTES) {
56
- void transport.send({ jsonrpc: "2.0", id: message.id, error: { code: -32600, message: "Card request exceeds the JSON body limit." } });
58
+ const budget = settingsUploadMessageBudget(message);
59
+ void transport.send({ jsonrpc: "2.0", id: message.id, error: { code: -32600,
60
+ message: budget === undefined ? "Card request exceeds the JSON body limit." : "settings_payload_too_large",
61
+ ...(budget === undefined ? {} : { data: { code: "settings_payload_too_large", maximumMessageSize: MAX_JSON_BODY_BYTES, rawChunkLimit: budget } }) } });
57
62
  return;
58
63
  }
59
64
  connection.onmessage?.(message, extra);
@@ -1,6 +1,6 @@
1
1
  import { createServer } from "node:http";
2
2
  import { randomUUID } from "node:crypto";
3
- import { HttpError, sendJson } from "../../review-server/http.js";
3
+ import { HttpError, sendJson } from "./http.js";
4
4
  import { createServerIdentity, IDENTITY_CHALLENGE_HEADER, IDENTITY_PATH, INTERNAL_MCP_PATH, singleHeader, validControlAuthorization, validInternalRequest } from "./control.js";
5
5
  export async function startSharedServer(input) {
6
6
  const instanceId = randomUUID();
@@ -122,7 +122,6 @@ async function main() {
122
122
  validateAdapterLifecycle: validateSupportedAdapterLifecycle,
123
123
  sessions: activityStore.createSessionRecordStore(),
124
124
  artifacts: activityStore.createPrivateReviewArtifactStore(),
125
- settingsStore: activityStore.createSettingsRecordStore()
126
125
  });
127
126
  const reviewDependencies = createRuntimeReviewDependencies({ client: suiClient, chainIdentifier,
128
127
  expectedChainIdentifier: config.expectedChainIdentifier, materialStore: transactionMaterialStore, readService });
@@ -12,12 +12,11 @@ const persistentKeys = new Set([
12
12
  ]);
13
13
  const storageSchema = "CREATE TABLE sdk_sessions (key TEXT PRIMARY KEY, value BLOB NOT NULL)";
14
14
  function storageFormat(db) {
15
- const version = db.pragma("user_version", { simple: true });
16
15
  const schema = db.prepare("SELECT type,name,sql FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%' ORDER BY type,name")
17
16
  .all();
18
- if (version === 0 && schema.length === 0)
17
+ if (schema.length === 0)
19
18
  return "empty";
20
- if (version === 1 && schema.length === 1 && schema[0]?.type === "table" &&
19
+ if (schema.length === 1 && schema[0]?.type === "table" &&
21
20
  schema[0].name === "sdk_sessions" && schema[0].sql === storageSchema)
22
21
  return "current";
23
22
  throw new Error("WalletConnect storage format is unsupported.");
@@ -59,7 +58,6 @@ export function openWalletConnectStorage(dataDirectory) {
59
58
  if (storageFormat(db) === "empty") {
60
59
  db.transaction(() => {
61
60
  db.exec(storageSchema);
62
- db.pragma("user_version = 1");
63
61
  })();
64
62
  }
65
63
  // There is exactly one SDK owner. FULL protects the session material that
@@ -1,3 +1,4 @@
1
+ import { SERVER_VERSION } from "../mcp/serverInfo.js";
1
2
  import { join } from "node:path";
2
3
  import { isDeepStrictEqual } from "node:util";
3
4
  import { WalletUserRejectedError } from "../core/session/walletConnection.js";
@@ -19,7 +20,7 @@ export function runWalletSdkChild(createTransport) {
19
20
  process.send(walletEventSchema.parse(message), (error) => { if (error)
20
21
  exit(); });
21
22
  };
22
- const envelope = () => ({ protocolVersion: 1, runId: init.runId });
23
+ const envelope = () => ({ packageVersion: SERVER_VERSION, runId: init.runId });
23
24
  const exit = () => { if (!exiting) {
24
25
  exiting = true;
25
26
  void ownership;
@@ -1,8 +1,9 @@
1
+ import { SERVER_VERSION } from "../mcp/serverInfo.js";
1
2
  import { z } from "zod";
2
3
  import { suiAddressStringSchema } from "../core/suiAddress.js";
3
4
  import { walletRunIdSchema, walletStartStageSchema, walletSnapshotSchema, walletSessionObservationSchema, walletSessionSchema } from "../core/session/walletRuntime.js";
4
5
  import { walletUnavailableReasonSchema } from "../core/session/walletConnection.js";
5
- const envelope = z.object({ protocolVersion: z.literal(1), runId: walletRunIdSchema });
6
+ const envelope = z.object({ packageVersion: z.literal(SERVER_VERSION), runId: walletRunIdSchema });
6
7
  const request = envelope.extend({ requestId: z.number().int().positive(), operationId: z.string().min(1) });
7
8
  export const walletCommandSchema = z.union([
8
9
  envelope.extend({ type: z.literal("init"), dataDirectory: z.string().min(1), projectId: z.string().regex(/^[0-9a-f]{32}$/i),
@@ -1,3 +1,4 @@
1
+ import { SERVER_VERSION } from "../mcp/serverInfo.js";
1
2
  import { fork } from "node:child_process";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import { isDeepStrictEqual } from "node:util";
@@ -80,7 +81,7 @@ export class WalletSdkProcess {
80
81
  });
81
82
  child.once("disconnect", () => { if (!run.fenced && !run.didExit)
82
83
  this.lose(run, this.failureReason()); });
83
- this.send(run, { protocolVersion: 1, runId: run.id, type: "init", dataDirectory: this.options.dataDirectory,
84
+ this.send(run, { packageVersion: SERVER_VERSION, runId: run.id, type: "init", dataDirectory: this.options.dataDirectory,
84
85
  projectId: this.options.projectId, metadata: this.options.metadata });
85
86
  }
86
87
  catch {
@@ -318,7 +319,7 @@ export class WalletSdkProcess {
318
319
  return new Promise((resolve, reject) => {
319
320
  run.pending.set(requestId, { operationId, type: input.type, resolve, reject, ...approval });
320
321
  try {
321
- this.send(run, { ...input, protocolVersion: 1, runId: run.id, requestId, operationId });
322
+ this.send(run, { ...input, packageVersion: SERVER_VERSION, runId: run.id, requestId, operationId });
322
323
  }
323
324
  catch (error) {
324
325
  run.pending.delete(requestId);
@@ -311,7 +311,7 @@ For a connected-account-only read, use `session.get_interaction_status` to disti
311
311
  1. `account.get_active_account` reports the stored selection, with `source` and `setAt`. For current assets without a user-provided address, use `session.get_interaction_status.assetReadAccount` to check whether that selection is a usable default. If not, request an address only for tools that accept `account`; connected-account-only tools require the connection/account-selection flow. Do not infer current connection from the stored selection.
312
312
  2. An explicit connect/reconnect/disconnect/account replacement request opens an internal
313
313
  Connect card with `session.create_wallet_connection`: use `intent: connect` for requested pairing, `intent: disconnect` for requested disconnection, and `intent: manage` for inspection or account selection. The model must not call app-only actions on the user's behalf.
314
- For "disconnect my wallet", when the wallet service is available and the target is connected, open `session.create_wallet_connection` with `intent: disconnect`. For one connected target the card shows Confirm disconnect or Back directly; Back only returns to the connection view; it does not submit Cancel review or disconnect. The card starts no pairing. Do not substitute `account.clear_active_account`, claim disconnection is unsupported merely because there is no direct model-facing disconnect tool, or equate disconnection with revoking onchain permissions. If disconnection is already pending, read or wait on that exact card instead of opening another operation.
314
+ For "disconnect my wallet", when the wallet service is available and the target is connected, open `session.create_wallet_connection` with `intent: disconnect`. For one connected target the card shows Confirm disconnect or Back directly; Back only returns to the connection view; it does not submit Cancel review or disconnect. The card starts no pairing. Do not substitute the Settings action to clear the default read address, claim disconnection is unsupported merely because there is no direct model-facing disconnect tool, or equate disconnection with revoking onchain permissions. If disconnection is already pending, read or wait on that exact card instead of opening another operation.
315
315
 
316
316
  3. `session.get_wallet_connection` and `session.wait_wallet_connection` use the
317
317
  returned cardId. An unsubmitted card needs input; waiting does not create a
@@ -347,12 +347,28 @@ them. `allowedActions` describes permitted action types, not all buttons visible
347
347
  in a particular frame. In particular, a card without a target does not show
348
348
  Disconnect or Use account merely because a general action name was mentioned.
349
349
 
350
- There is no service-restart button in Connect or Settings. If the service is
351
- unavailable or disconnection remains unresponsive, explain the operational
352
- procedure in the setup guide: fully quit all apps using Sui MCP, then
353
- reopen them. Closing only a non-owning app or a window may leave the shared
354
- backend running. Do not kill processes or repeatedly send wallet commands for
355
- the user, and do not report restart success before the new service is observed.
350
+ Connection-service recovery belongs to Settings. When the service is unavailable
351
+ or a connection operation remains unresponsive, open
352
+ `settings.create_local_settings_session` on the user's request and direct them
353
+ to Troubleshooting. The user inspects the affected run/requests and explicitly
354
+ confirms Restart connection service; do not call app-only actions for them.
355
+ A new Settings card can explicitly supersede a startup still pending. Read the
356
+ same operation's result; neither a new available run nor restart completion
357
+ confirms an interrupted disconnect or a transaction result. Do not repeatedly
358
+ request wallet approval or report recovery before its new run is observed.
359
+ `manageableConnectionIds` names exact disconnect targets, not usable wallet
360
+ choices. A terminal record may remain manageable when its SDK session is still
361
+ present; do not describe the old failed operation as successful. If
362
+ `connectionReconciliationRequired` is true, explain that the service contains an
363
+ unrecorded connection. Ask the user to remove that retained connection in the
364
+ wallet app, then check its status. Settings/Troubleshooting can restart the
365
+ service to inspect it again; restarting does not approve an unrecorded session.
366
+ Do not infer a free pairing slot from a failed or expired record.
367
+
368
+ If the parent or Settings itself cannot be reached, explain the full app exit
369
+ procedure in the setup guide. All participating apps must quit; closing a peer
370
+ or window can leave the owner alive. Full app restart is also required for
371
+ stored endpoint or installation changes. Do not kill another app's processes.
356
372
 
357
373
  A failed `connectionAction: "disconnect"` means removal was not confirmed.
358
374
  Direct the user to inspect/remove the retained connection in their wallet app.
@@ -468,9 +484,27 @@ For `read.list_review_activity`, `dataScope.recordCount` is the full matching lo
468
484
 
469
485
  For `read.get_review_session_detail`, transition rows with `isNoOp: true` are repeated lifecycle observations where the stored status did not change. Mention them only as audit details; do not count them as extra funnel progress.
470
486
 
471
- For local settings or local data management, call `settings.create_local_settings_session` and tell the user to open the returned settings URL in the same machine's system browser.
472
-
473
- Do not call MCP tools to mutate settings directly; direct settings mutator tools are not exposed. Do not call clearing active account a wallet disconnect. Endpoint changes apply after MCP server restart.
487
+ For local settings, local records or connection-service recovery, call `settings.create_local_settings_session` to open the internal Settings card. Its name is unchanged; no external URL is returned. Explain Address for viewing assets, Local records, Troubleshooting or Advanced settings as needed and leave selection/confirmation to the user.
488
+
489
+ For a backup, use the exact label Download backup in Local records. It prepares
490
+ and verifies the snapshot internally and asks the app to save it in one user
491
+ action. Do not instruct a separate Create backup step or confuse Download backup
492
+ with Restore local records. If the actual download function is unsupported,
493
+ the same file is saved once in the Sui MCP backup folder. Use the verified
494
+ Backup saved result's Folder and File name for that local artifact. Do not
495
+ describe that folder as the Host download destination. Suggested file name and Host acknowledgment do not establish where
496
+ or whether a file was saved. Retry download is explicit and may create another
497
+ copy after an unconfirmed earlier request. It uses the same snapshot only during
498
+ its fixed transfer period; a new request after expiry or server restart captures
499
+ current records. Cancel download does not delete a file already saved by the app.
500
+ Use the same function-based product instructions across desktop apps. Missing
501
+ advertisement is not proof of non-support, and timeout, cancellation or denial
502
+ does not trigger local saving. Save a local copy is the explicit decision after
503
+ an earlier possibly handled request. Retry save follows only a confirmed failure
504
+ with no published file; Check status settles uncertain saving or cleanup without
505
+ another file effect. Report saved-file and cleanup facts separately.
506
+
507
+ Ordinary MCP tools never apply a Settings change. Do not call `ui.act_card` or `ui.read_settings_file` as model work. Never request/paste raw backup bytes or card permissions in chat. Clear default address only changes read context, not wallet connections or admitted transactions. Endpoint changes require quitting/reopening all apps; a connection-service restart does not apply them. Report operation result, temporary-file cleanup and Host download acknowledgment as separate facts. A saved result is not proof that the Host saved a downloaded file. For restore validation refusal, let the user correct input or choose another file and prepare a new confirmation; reading or returning to a frame never applies it.
474
508
 
475
509
  ## User Vocabulary
476
510