@zktx.io/sui-mcp 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +21 -21
  2. package/dist/adapters/adapterPromptSurfaces.js +0 -20
  3. package/dist/adapters/deepbook/deepbookQuotePolicy.js +0 -4
  4. package/dist/adapters/deepbook/deepbookReviewEvidence.js +0 -9
  5. package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +0 -9
  6. package/dist/adapters/intentPlanFactories.js +0 -6
  7. package/dist/core/action/contractNameRegistry.js +0 -53
  8. package/dist/core/action/forbiddenFields.js +0 -4
  9. package/dist/core/action/ptbVisualizationProducer.js +0 -3
  10. package/dist/core/action/receiptPtbGraph.js +0 -17
  11. package/dist/core/action/reviewTimeSimulationEvidence.js +0 -4
  12. package/dist/core/action/signableAdapterContract.js +0 -5
  13. package/dist/core/action/suiChainReceiptMapping.js +0 -18
  14. package/dist/core/action/suiChainReceiptReader.js +1 -21
  15. package/dist/core/action/suiChainReceiptVerifier.js +0 -10
  16. package/dist/core/action/transactionFunding.js +0 -7
  17. package/dist/core/action/types.js +0 -1
  18. package/dist/core/action/walletReviewContractAssembler.js +0 -2
  19. package/dist/core/activity/localDataService.js +65 -52
  20. package/dist/core/activity/localDataValidation.js +1 -3
  21. package/dist/core/activity/sqliteActivityStore.js +24 -24
  22. package/dist/core/activity/sqliteActivityStoreRows.js +0 -2
  23. package/dist/core/activity/sqliteActivityStoreSchema.js +81 -14
  24. package/dist/core/preferences/sqlitePreferencesRepository.js +5 -3
  25. package/dist/core/read/deepbookReadHelpers.js +0 -2
  26. package/dist/core/read/deepbookRegistry.js +0 -19
  27. package/dist/core/read/deepbookUsdcChartService.js +0 -2
  28. package/dist/core/read/readCardResult.js +0 -2
  29. package/dist/core/read/readService.js +0 -19
  30. package/dist/core/read/readServiceTypes.js +0 -2
  31. package/dist/core/session/cardSession.js +1 -5
  32. package/dist/core/session/cardSessionStore.js +54 -7
  33. package/dist/core/session/reviewValidity.js +0 -3
  34. package/dist/core/session/sessionRecordStore.js +0 -1
  35. package/dist/core/session/sessionStore.js +0 -32
  36. package/dist/core/session/settingsFileIdentity.js +9 -0
  37. package/dist/core/session/settingsFileProtocol.js +20 -0
  38. package/dist/core/session/settingsFileStore.js +337 -0
  39. package/dist/core/session/settingsInputError.js +32 -0
  40. package/dist/core/session/settingsOperation.js +220 -0
  41. package/dist/core/session/settingsPreparationSql.js +13 -0
  42. package/dist/core/session/settingsWorkflow.js +1183 -0
  43. package/dist/core/session/sqliteCardStore.js +0 -2
  44. package/dist/core/session/sqliteSessionStore.js +0 -51
  45. package/dist/core/session/sqliteSettingsWorkflowStore.js +453 -0
  46. package/dist/core/session/sqliteTransactionMaterialStore.js +0 -11
  47. package/dist/core/session/sqliteWalletWorkflowStore.js +160 -66
  48. package/dist/core/session/status.js +2 -4
  49. package/dist/core/session/transactionMaterialStore.js +0 -4
  50. package/dist/core/session/transactionRequest.js +0 -4
  51. package/dist/core/session/walletConnection.js +26 -10
  52. package/dist/core/session/walletWorkflow.js +70 -30
  53. package/dist/core/session/workflowState.js +14 -14
  54. package/dist/core/session/workflowView.js +18 -8
  55. package/dist/core/suiAddress.js +0 -5
  56. package/dist/core/suiEndpoint.js +0 -3
  57. package/dist/mcp/prompts.js +0 -12
  58. package/dist/mcp/responseGuidance.js +10 -5
  59. package/dist/mcp/schemas.js +0 -3
  60. package/dist/mcp/serverInfo.js +3 -4
  61. package/dist/mcp/toolNames.js +2 -2
  62. package/dist/mcp/tools/account/index.js +1 -19
  63. package/dist/mcp/tools/action/prepareSuiActionReview.js +0 -6
  64. package/dist/mcp/tools/read/readToolHelpers.js +5 -4
  65. package/dist/mcp/tools/read/serverStatusTools.js +0 -1
  66. package/dist/mcp/tools/session/executionResultTools.js +2 -2
  67. package/dist/mcp/tools/session/shared.js +1 -2
  68. package/dist/mcp/tools/session/statusTools.js +4 -5
  69. package/dist/mcp/tools/settings/index.js +8 -28
  70. package/dist/mcp-app/account.html +14 -14
  71. package/dist/mcp-app/account.notices.txt +1 -1
  72. package/dist/mcp-app/chart.html +9 -9
  73. package/dist/mcp-app/chart.notices.txt +1 -1
  74. package/dist/mcp-app/connect.html +14 -14
  75. package/dist/mcp-app/connect.notices.txt +1 -1
  76. package/dist/mcp-app/receipt.html +1298 -1463
  77. package/dist/mcp-app/receipt.notices.txt +226 -1204
  78. package/dist/mcp-app/review.html +1298 -1463
  79. package/dist/mcp-app/review.notices.txt +226 -1204
  80. package/dist/mcp-app/settings.html +3133 -0
  81. package/dist/mcp-app/settings.notices.txt +973 -0
  82. package/dist/mcp-ui/contracts.js +8 -2
  83. package/dist/mcp-ui/readCards.js +1 -1
  84. package/dist/mcp-ui/tools.js +34 -4
  85. package/dist/review-server/server.js +3 -51
  86. package/dist/runtime/application.js +41 -14
  87. package/dist/runtime/config.js +0 -2
  88. package/dist/runtime/localSettingsService.js +23 -2
  89. package/dist/runtime/logger.js +0 -2
  90. package/dist/runtime/reviewServerAcquire.js +1 -18
  91. package/dist/runtime/runtimeTermination.js +1 -4
  92. package/dist/runtime/shared/authenticatedFetch.js +2 -4
  93. package/dist/runtime/shared/control.js +4 -7
  94. package/dist/runtime/shared/dataAccess.js +0 -2
  95. package/dist/runtime/shared/http.js +14 -0
  96. package/dist/runtime/shared/mcpHttp.js +6 -6
  97. package/dist/runtime/shared/ownerLease.js +0 -4
  98. package/dist/runtime/shared/server.js +1 -2
  99. package/dist/runtime/shared/stdio.js +2 -5
  100. package/dist/runtime/smokeMainnetRead.js +0 -3
  101. package/dist/runtime/smokeReview.js +0 -4
  102. package/dist/runtime/start.js +0 -2
  103. package/dist/runtime/suiTransactionGraphqlMapping.js +0 -2
  104. package/dist/runtime/suiTransactionGraphqlQueries.js +0 -3
  105. package/dist/runtime/walletConnectConfig.js +0 -2
  106. package/dist/runtime/walletConnectStorage.js +2 -10
  107. package/dist/runtime/walletConnectTransport.js +0 -11
  108. package/dist/runtime/walletSdkChildRuntime.js +2 -6
  109. package/dist/runtime/walletSdkIpc.js +2 -1
  110. package/dist/runtime/walletSdkProcess.js +3 -8
  111. package/docs/AGENT_BEHAVIOR.md +45 -15
  112. package/docs/LOCAL_DB_ARCHITECTURE.md +145 -22
  113. package/docs/MCP_SETUP.md +99 -39
  114. package/docs/MCP_TOOLS.md +288 -24
  115. package/docs/SDK_API.md +63 -10
  116. package/docs/UTILITY_INDEX.md +9 -6
  117. package/docs/WALLET_CONNECTION.md +32 -10
  118. package/package.json +22 -8
  119. package/dist/core/activity/schemaVersion.js +0 -2
  120. package/dist/core/session/keyedRecordStore.js +0 -18
  121. package/dist/core/session/settingsSession.js +0 -1
  122. package/dist/core/session/settingsSessions.js +0 -45
  123. package/dist/review-app/brand-dark.svg +0 -1
  124. package/dist/review-app/brand-light.svg +0 -1
  125. package/dist/review-app/favicon.svg +0 -1
  126. package/dist/review-app/settings.css +0 -1
  127. package/dist/review-app/settings.js +0 -1
  128. package/dist/review-app/ui.css +0 -809
  129. package/dist/review-server/assets.js +0 -48
  130. package/dist/review-server/html.js +0 -45
  131. package/dist/review-server/http.js +0 -47
  132. package/dist/review-server/middleware/reviewToken.js +0 -7
  133. package/dist/review-server/settingsApi.js +0 -158
  134. package/docs/AGENT_DEVELOPMENT_POLICY.md +0 -815
  135. package/docs/FRONTEND_POLICY.md +0 -534
  136. package/docs/golden-scenarios/BEHAVIOR_MATRIX.md +0 -166
  137. package/docs/golden-scenarios/INTENT_EVIDENCE_GOLDEN_ANSWERS.md +0 -161
  138. package/docs/golden-scenarios/INTENT_EVIDENCE_MATRIX.md +0 -259
@@ -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]
@@ -19,8 +21,9 @@ export function supportsCards(server) {
19
21
  const capabilities = server.server.getClientCapabilities();
20
22
  const { extensions, ...standard } = capabilities ?? {};
21
23
  const current = capabilities === undefined ? undefined : { ...standard, ...(extensions === undefined ? {} : { extensions }) };
22
- return getUiCapability(current)?.mimeTypes?.includes(RESOURCE_MIME_TYPE) === true ||
23
- server.server.getClientVersion()?.name === "codex-mcp-client";
24
+ const ui = getUiCapability(current);
25
+ return ui !== null && typeof ui === "object" && !Array.isArray(ui) && Array.isArray(ui.mimeTypes) &&
26
+ ui.mimeTypes.every((mimeType) => typeof mimeType === "string") && ui.mimeTypes.includes(RESOURCE_MIME_TYPE);
24
27
  }
25
28
  export function cardToolResult(response, savedLink = false) {
26
29
  const snapshot = response.snapshot;
@@ -131,4 +134,31 @@ export function registerReadCards(server, deps) {
131
134
  }
132
135
  });
133
136
  }
137
+ const fileInput = cardReferenceSchema.extend({ attemptId: z.string().uuid().optional(), operationId: z.string().uuid(), fileId: z.string().uuid(), offset: z.number().int().nonnegative().safe(),
138
+ limit: z.number().int().positive().safe().optional() }).strict();
139
+ registerAppTool(server, CARD_TOOLS.readSettingsFile, { description: "Read a verified backup file for its current download attempt.", inputSchema: fileInput,
140
+ _meta: { ui: { visibility: ["app"] } }, annotations: { readOnlyHint: true, openWorldHint: false }
141
+ }, async (input, extra) => {
142
+ try {
143
+ if (!deps.cards)
144
+ throw new CardError("Card service is unavailable.");
145
+ const base = okToolResult({ status: "read", fileId: input.fileId, offset: input.offset, rangeSize: Number.MAX_SAFE_INTEGER });
146
+ const metadata = { ...(input.attemptId ? { attemptId: input.attemptId } : {}), cardId: input.cardId, operationId: input.operationId, fileId: input.fileId, offset: input.offset, totalSize: Number.MAX_SAFE_INTEGER,
147
+ sha256: "0".repeat(64), chunkBase64: "" };
148
+ base._meta = { [SETTINGS_FILE_METADATA_KEY]: metadata };
149
+ const budget = settingsRawChunkBudget({ jsonrpc: "2.0", id: extra.requestId, result: base });
150
+ if (!budget)
151
+ throw new CardError("The backup response cannot fit the app's message envelope.");
152
+ const read = await deps.cards.store.readSettingsFile(input, Math.min(input.limit ?? budget, budget));
153
+ const rangeSize = Buffer.from(read.chunkBase64, "base64").length;
154
+ const result = okToolResult({ status: "read", fileId: input.fileId, offset: input.offset, rangeSize });
155
+ result._meta = { [SETTINGS_FILE_METADATA_KEY]: { ...metadata, totalSize: read.file.totalSize, sha256: read.file.sha256, chunkBase64: read.chunkBase64 } };
156
+ if (settingsJsonSize({ jsonrpc: "2.0", id: extra.requestId, result }) > MAX_JSON_BODY_BYTES)
157
+ throw new CardError("The backup response exceeds its message limit.");
158
+ return result;
159
+ }
160
+ catch {
161
+ return errorToolResult({ kind: "input_invalid", details: { reason: "This backup file could not be read or verified." } });
162
+ }
163
+ });
134
164
  }
@@ -1,10 +1,6 @@
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";
8
4
  export function createReviewRequestHandler(options) {
9
5
  return async (request, response) => {
10
6
  try {
@@ -18,55 +14,11 @@ export function createReviewRequestHandler(options) {
18
14
  sendJson(response, 400, { error: "token_query_not_supported" });
19
15
  return;
20
16
  }
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
17
  sendJson(response, 404, { error: "not_found" });
49
18
  }
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" });
19
+ catch {
20
+ options.logger.error("HTTP routing failed", { stage: "http_routing" });
56
21
  sendJson(response, 500, { error: "internal_error" });
57
22
  }
58
23
  };
59
24
  }
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 { }
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
  }
@@ -17,8 +17,6 @@ export function loadBootConfig(env = process.env) {
17
17
  activityDatabasePath: resolveActivityDatabasePath(env)
18
18
  };
19
19
  }
20
- // Existing shared-backend port: clients using the same data directory must
21
- // agree on this port and configuration. It is not browser-wallet authority.
22
20
  export const DEFAULT_REVIEW_PORT = 8765;
23
21
  function parseReviewPort(value) {
24
22
  if (value === undefined || value === "") {
@@ -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,18 @@ export class RuntimeLocalSettingsService {
48
57
  suiGraphqlUrl: withBootEffectiveEndpoint(currentStoredGraphqlView, this.bootSuiGraphqlUrl)
49
58
  };
50
59
  }
60
+ resolveEndpointChange(input) {
61
+ const value = input.kind === "restore_endpoint"
62
+ ? input.endpoint === "grpc" ? this.defaultSuiGrpcUrl : this.defaultSuiGraphqlUrl
63
+ : input.endpoint === "grpc" ? parseEndpointForTool(input.value ?? "") : parseGraphqlEndpointForTool(input.value ?? "");
64
+ return { key: input.endpoint === "grpc" ? "suiGrpcUrl" : "suiGraphqlUrl", value,
65
+ status: input.kind === "restore_endpoint" ? "reset" : "saved" };
66
+ }
67
+ async prepareEndpointChange(input) {
68
+ const target = this.resolveEndpointChange(input);
69
+ await this.verifyEndpointForTool(target.value, input.endpoint);
70
+ return target;
71
+ }
51
72
  async setSuiGrpcUrl(url) {
52
73
  const normalized = parseEndpointForTool(url);
53
74
  await this.verifyEndpointForTool(normalized, "grpc");
@@ -3,8 +3,6 @@ export function flushStderr() {
3
3
  const failed = (error) => { process.stderr.off("error", failed); reject(error); };
4
4
  process.stderr.once("error", failed);
5
5
  process.stderr.write("", (error) => {
6
- // A failed stream write also emits error; keep its handler through that
7
- // event instead of allowing EPIPE to replace the intended exit code.
8
6
  if (error) {
9
7
  reject(error);
10
8
  return;
@@ -4,8 +4,6 @@ function errorCode(error) {
4
4
  ? error.code
5
5
  : undefined;
6
6
  }
7
- // Bind the port: returns the server on success, undefined when the port is already
8
- // in use, and rethrows any other (non-EADDRINUSE) startup error.
9
7
  async function tryStart(start, port) {
10
8
  try {
11
9
  return await start(port);
@@ -17,17 +15,6 @@ async function tryStart(start, port) {
17
15
  throw error;
18
16
  }
19
17
  }
20
- /**
21
- * Bind the review server to its fixed port, or defer to a healthy peer already
22
- * serving it.
23
- *
24
- * - Port free → bind and own the single review origin.
25
- * - Port held by a separate healthy instance of our review server → defer (run no
26
- * local server; the peer serves the shared database for every client) and watch for
27
- * the owner to exit, then take the origin over. No peer process is signalled.
28
- * - Port held by anything else (foreign, no identity answer, or our own pid) → clear
29
- * error; the origin is never silently reassigned.
30
- */
31
18
  export async function startOrDeferReviewServer(start, port, deps) {
32
19
  const owned = await tryStart(start, port);
33
20
  if (owned) {
@@ -58,14 +45,12 @@ export async function startOrDeferReviewServer(start, port, deps) {
58
45
  next = await tryStart(start, port);
59
46
  }
60
47
  catch (error) {
61
- // An authenticated prior owner may have closed its port while its SDK
62
- // still owns private storage. Reuse the existing takeover observation.
63
48
  if (typeof error === "object" && error !== null && "code" in error && error.code === "DATA_DIRECTORY_OWNED")
64
49
  continue;
65
50
  throw error;
66
51
  }
67
52
  if (!next) {
68
- continue; // a peer still owns the port; keep deferring
53
+ continue;
69
54
  }
70
55
  if (stopped) {
71
56
  await next.close();
@@ -82,8 +67,6 @@ export async function startOrDeferReviewServer(start, port, deps) {
82
67
  return {
83
68
  deferred: true,
84
69
  close: async () => {
85
- // Stop watching; the loop's stopped-check closes any bind that lands in-flight,
86
- // so we never await the (possibly mid-delay) watch loop here.
87
70
  stopped = true;
88
71
  if (acquired) {
89
72
  await acquired.close();
@@ -1,5 +1,4 @@
1
1
  import { SuiEndpointError } from "../core/suiEndpoint.js";
2
- // Keep arbitrary SDK/server exception text, endpoint values and stacks private.
3
2
  export function runtimeFailureReason(error, stage) {
4
3
  if (error instanceof SuiEndpointError)
5
4
  return `Sui mainnet endpoint verification failed (${error.kind}). Check the configured endpoint.`;
@@ -16,8 +15,6 @@ export function runtimeFailureReason(error, stage) {
16
15
  return "The private runtime control identity could not be loaded. Check data directory ownership and permissions.";
17
16
  return "The local runtime could not complete this stage. Check local data permissions and runtime configuration.";
18
17
  }
19
- // Bootstrap alone decides exit intent. Cleanup-induced onclose notifications
20
- // join the same promise; they cannot replace a fatal or signal exit with EOF.
21
18
  export class RuntimeTermination {
22
19
  options;
23
20
  closing;
@@ -34,7 +31,7 @@ export class RuntimeTermination {
34
31
  try {
35
32
  this.options.logger.error(message, { stage, reason: runtimeFailureReason(error, stage) });
36
33
  }
37
- catch { /* Preserve failure intent even if stderr is unavailable. */ }
34
+ catch { }
38
35
  };
39
36
  if (cause.kind === "failure")
40
37
  report("fatal runtime error", cause.stage, cause.error);
@@ -2,10 +2,8 @@ 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
- // Proof and authenticated request use the same TCP socket. A closed socket is never
8
- // transparently replaced: no credential or UI permission can reach a new listener.
9
7
  async function authenticatedChannel(port, control, signal) {
10
8
  signal?.throwIfAborted();
11
9
  const socket = connect({ host: "127.0.0.1", port });
@@ -86,7 +84,7 @@ export function createAuthenticatedFetch(port, control, expectedInstanceId) {
86
84
  headers.set("host", url.host);
87
85
  headers.set("authorization", `Bearer ${control.key}`);
88
86
  headers.set(SERVER_INSTANCE_HEADER, channel.identity.instanceId);
89
- headers.set("connection", "close");
87
+ headers.set("connection", "keep-alive");
90
88
  const body = init?.body;
91
89
  if (body !== undefined && body !== null && typeof body !== "string") {
92
90
  channel.close();
@@ -2,20 +2,17 @@ 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";
8
6
  export const IDENTITY_PATH = "/__runtime/identity";
9
7
  export const INTERNAL_MCP_PATH = "/__runtime/mcp";
10
8
  export const IDENTITY_CHALLENGE_HEADER = "x-sui-mcp-identity-challenge";
11
9
  export const SERVER_INSTANCE_HEADER = "x-sui-mcp-server-instance";
12
- // The existing review-server identity probe allows one second for a local response.
13
10
  export const IDENTITY_TIMEOUT_MS = 1000;
14
11
  const secretSchema = z.string().regex(/^[A-Za-z0-9_-]{43}$/);
15
12
  export const serverIdentitySchema = z.object({
16
13
  service: z.literal("sui-mcp"),
17
14
  role: z.literal("shared-server"),
18
- apiVersion: z.literal(INTERNAL_API_VERSION),
15
+ packageVersion: z.literal(SERVER_VERSION),
19
16
  databaseId: z.string().regex(/^[0-9a-f]{64}$/),
20
17
  configurationId: z.string().regex(/^[0-9a-f]{64}$/),
21
18
  instanceId: z.string().uuid(),
@@ -53,12 +50,12 @@ export async function loadControlIdentity(databasePath, configuration) {
53
50
  };
54
51
  }
55
52
  function identityPayload(identity) {
56
- return JSON.stringify([identity.service, identity.role, identity.apiVersion, identity.databaseId,
53
+ return JSON.stringify([identity.service, identity.role, identity.packageVersion, identity.databaseId,
57
54
  identity.configurationId, identity.instanceId, identity.pid, identity.challenge]);
58
55
  }
59
56
  export function createServerIdentity(control, instanceId, challenge) {
60
57
  secretSchema.parse(challenge);
61
- const identity = { service: "sui-mcp", role: "shared-server", apiVersion: INTERNAL_API_VERSION,
58
+ const identity = { service: "sui-mcp", role: "shared-server", packageVersion: SERVER_VERSION,
62
59
  databaseId: control.databaseId, configurationId: control.configurationId,
63
60
  instanceId, pid: process.pid, challenge };
64
61
  return { ...identity, proof: createHmac("sha256", control.key).update(identityPayload(identity)).digest("base64url") };
@@ -1,6 +1,4 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
- // An asynchronous read may finish after import/reset. Its original generation
3
- // remains attached to the continuation and cannot read or write the replacement DB.
4
2
  export class RuntimeDataAccess {
5
3
  context = new AsyncLocalStorage();
6
4
  generation = 0;
@@ -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,14 +2,13 @@ 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();
9
10
  return {
10
11
  async handle(request, response) {
11
- // This product has no unsolicited server requests. POST returns JSON, and
12
- // clients do not create a reconnecting SSE channel or replay requests.
13
12
  if (request.method === "GET") {
14
13
  response.setHeader("Allow", "POST, DELETE");
15
14
  sendJson(response, 405, { error: "method_not_allowed" });
@@ -48,12 +47,13 @@ export function createInternalMcpHandler(createServer) {
48
47
  close: () => transport.close()
49
48
  };
50
49
  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.
53
50
  if (isJSONRPCRequest(message) && message.method === "tools/call" &&
54
51
  Object.values(CARD_TOOLS).some((name) => name === message.params?.name) &&
55
52
  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." } });
53
+ const budget = settingsUploadMessageBudget(message);
54
+ void transport.send({ jsonrpc: "2.0", id: message.id, error: { code: -32600,
55
+ message: budget === undefined ? "Card request exceeds the JSON body limit." : "settings_payload_too_large",
56
+ ...(budget === undefined ? {} : { data: { code: "settings_payload_too_large", maximumMessageSize: MAX_JSON_BODY_BYTES, rawChunkLimit: budget } }) } });
57
57
  return;
58
58
  }
59
59
  connection.onmessage?.(message, extra);
@@ -4,8 +4,6 @@ import { dirname, join } from "node:path";
4
4
  export class DataDirectoryOwnedError extends Error {
5
5
  code = "DATA_DIRECTORY_OWNED";
6
6
  }
7
- /** An OS-released ownership lock, separate from the product database. No
8
- * business transaction is held while waiting for a wallet or network reply. */
9
7
  export function acquireDataDirectoryOwner(databasePath) {
10
8
  const directory = dirname(databasePath);
11
9
  mkdirSync(directory, { recursive: true, mode: 0o700 });
@@ -31,8 +29,6 @@ export function acquireDataDirectoryOwner(databasePath) {
31
29
  process.umask(mask);
32
30
  }
33
31
  try {
34
- // A second owner is refused immediately, including one using another port.
35
- // SQLite retains the EXCLUSIVE lock after commit until this handle closes.
36
32
  lock.pragma("locking_mode=EXCLUSIVE");
37
33
  lock.exec("BEGIN EXCLUSIVE");
38
34
  lock.exec("CREATE TABLE IF NOT EXISTS owner_lock (singleton INTEGER PRIMARY KEY CHECK (singleton=1))");
@@ -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();
@@ -64,7 +64,6 @@ export async function startSharedServer(input) {
64
64
  server.listen(port, "127.0.0.1");
65
65
  });
66
66
  port = server.address().port;
67
- // Only the process that owns the port opens SQLite or initializes services.
68
67
  application = Promise.resolve().then(() => input.createApplication(instanceId));
69
68
  const closeListener = async () => {
70
69
  if (!server.listening)
@@ -5,9 +5,6 @@ import { CallToolRequestSchema, CallToolResultSchema, CompleteRequestSchema, Get
5
5
  import { SERVER_INSTRUCTIONS, SERVER_NAME, SERVER_VERSION } from "../../mcp/serverInfo.js";
6
6
  import { createAuthenticatedFetch, probeAuthenticatedServer } from "./authenticatedFetch.js";
7
7
  import { INTERNAL_MCP_PATH } from "./control.js";
8
- // The SDK owns request IDs, initialization, cancellation and response parsing.
9
- // This proxy has no domain services or writable stores. A new request may open a
10
- // new MCP session after ownership changes; an already dispatched call is never replayed.
11
8
  export async function startSharedStdio(input) {
12
9
  const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, {
13
10
  capabilities: { tools: {}, resources: {}, prompts: {}, completions: {} }, instructions: SERVER_INSTRUCTIONS
@@ -56,7 +53,7 @@ export async function startSharedStdio(input) {
56
53
  close: async () => { try {
57
54
  await http.terminateSession();
58
55
  }
59
- catch { /* The prior owner may already be gone. */ } await http.close(); }
56
+ catch { } await http.close(); }
60
57
  };
61
58
  http.onmessage = (message) => { if (http.sessionId !== undefined)
62
59
  transport.sessionId = http.sessionId; transport.onmessage?.(message); };
@@ -105,7 +102,7 @@ export async function startSharedStdio(input) {
105
102
  try {
106
103
  await connecting;
107
104
  }
108
- catch { /* Initialization was cancelled with the stdio connection. */ }
105
+ catch { }
109
106
  await connection?.client.close();
110
107
  connection = undefined;
111
108
  }
@@ -122,12 +122,9 @@ 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 });
129
- // Utility-only read context in this script's temporary DB. This does not
130
- // create a live wallet session or prove address ownership.
131
128
  await activityStore.setActiveAccount(smokeAddress, "wallet_connection", new Date(), { name: "Explicit smoke read fixture" });
132
129
  server = createMcpServer({
133
130
  sessions,