@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.
- package/README.md +6 -6
- package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +3 -3
- package/dist/core/activity/localDataService.js +71 -52
- package/dist/core/activity/localDataValidation.js +1 -3
- package/dist/core/activity/sqliteActivityStore.js +24 -16
- package/dist/core/activity/sqliteActivityStoreSchema.js +83 -12
- package/dist/core/preferences/sqlitePreferencesRepository.js +5 -3
- package/dist/core/session/cardSession.js +1 -1
- package/dist/core/session/cardSessionStore.js +53 -3
- package/dist/core/session/sessionStore.js +0 -17
- package/dist/core/session/settingsFileIdentity.js +11 -0
- package/dist/core/session/settingsFileProtocol.js +25 -0
- package/dist/core/session/settingsFileStore.js +346 -0
- package/dist/core/session/settingsInputError.js +34 -0
- package/dist/core/session/settingsOperation.js +224 -0
- package/dist/core/session/settingsPreparationSql.js +15 -0
- package/dist/core/session/settingsWorkflow.js +1219 -0
- package/dist/core/session/sqliteSessionStore.js +0 -40
- package/dist/core/session/sqliteSettingsWorkflowStore.js +463 -0
- package/dist/core/session/sqliteWalletWorkflowStore.js +164 -41
- package/dist/core/session/status.js +2 -2
- package/dist/core/session/walletConnection.js +27 -8
- package/dist/core/session/walletWorkflow.js +73 -15
- package/dist/core/session/workflowState.js +16 -8
- package/dist/core/session/workflowView.js +18 -2
- package/dist/mcp/responseGuidance.js +10 -5
- package/dist/mcp/serverInfo.js +3 -4
- package/dist/mcp/toolNames.js +2 -2
- package/dist/mcp/tools/account/index.js +1 -19
- package/dist/mcp/tools/read/readToolHelpers.js +5 -4
- package/dist/mcp/tools/session/executionResultTools.js +2 -2
- package/dist/mcp/tools/session/shared.js +1 -0
- package/dist/mcp/tools/session/statusTools.js +4 -3
- package/dist/mcp/tools/settings/index.js +8 -28
- package/dist/mcp-app/account.html +14 -14
- package/dist/mcp-app/account.notices.txt +1 -1
- package/dist/mcp-app/chart.html +9 -9
- package/dist/mcp-app/chart.notices.txt +1 -1
- package/dist/mcp-app/connect.html +14 -14
- package/dist/mcp-app/connect.notices.txt +1 -1
- package/dist/mcp-app/receipt.html +1298 -1463
- package/dist/mcp-app/receipt.notices.txt +226 -1204
- package/dist/mcp-app/review.html +1298 -1463
- package/dist/mcp-app/review.notices.txt +226 -1204
- package/dist/mcp-app/settings.html +3133 -0
- package/dist/mcp-app/settings.notices.txt +973 -0
- package/dist/mcp-ui/contracts.js +8 -2
- package/dist/mcp-ui/readCards.js +1 -1
- package/dist/mcp-ui/tools.js +33 -2
- package/dist/review-server/server.js +5 -51
- package/dist/runtime/application.js +41 -14
- package/dist/runtime/localSettingsService.js +26 -2
- package/dist/runtime/shared/authenticatedFetch.js +6 -2
- package/dist/runtime/shared/control.js +5 -6
- package/dist/runtime/shared/http.js +14 -0
- package/dist/runtime/shared/mcpHttp.js +9 -4
- package/dist/runtime/shared/server.js +1 -1
- package/dist/runtime/smokeMainnetRead.js +0 -1
- package/dist/runtime/walletConnectStorage.js +2 -4
- package/dist/runtime/walletSdkChildRuntime.js +2 -1
- package/dist/runtime/walletSdkIpc.js +2 -1
- package/dist/runtime/walletSdkProcess.js +3 -2
- package/docs/AGENT_BEHAVIOR.md +44 -10
- package/docs/AGENT_DEVELOPMENT_POLICY.md +16 -12
- package/docs/FRONTEND_POLICY.md +105 -28
- package/docs/LOCAL_DB_ARCHITECTURE.md +143 -20
- package/docs/MCP_SETUP.md +99 -39
- package/docs/MCP_TOOLS.md +273 -24
- package/docs/SDK_API.md +51 -10
- package/docs/UTILITY_INDEX.md +9 -6
- package/docs/WALLET_CONNECTION.md +30 -9
- package/package.json +13 -7
- package/dist/core/activity/schemaVersion.js +0 -2
- package/dist/core/session/keyedRecordStore.js +0 -18
- package/dist/core/session/settingsSession.js +0 -1
- package/dist/core/session/settingsSessions.js +0 -45
- package/dist/review-app/brand-dark.svg +0 -1
- package/dist/review-app/brand-light.svg +0 -1
- package/dist/review-app/favicon.svg +0 -1
- package/dist/review-app/settings.css +0 -1
- package/dist/review-app/settings.js +0 -1
- package/dist/review-app/ui.css +0 -809
- package/dist/review-server/assets.js +0 -48
- package/dist/review-server/html.js +0 -45
- package/dist/review-server/http.js +0 -47
- package/dist/review-server/middleware/reviewToken.js +0 -7
- package/dist/review-server/settingsApi.js +0 -158
package/dist/mcp-ui/contracts.js
CHANGED
|
@@ -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
|
};
|
package/dist/mcp-ui/readCards.js
CHANGED
|
@@ -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" };
|
package/dist/mcp-ui/tools.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
51
|
-
|
|
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({
|
|
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
|
|
197
|
+
await settings.close();
|
|
177
198
|
}
|
|
178
199
|
finally {
|
|
200
|
+
access.close();
|
|
179
201
|
try {
|
|
180
|
-
await
|
|
202
|
+
await walletRuntime.close();
|
|
181
203
|
}
|
|
182
204
|
finally {
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
//
|
|
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
|
-
|
|
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.
|
|
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",
|
|
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 "
|
|
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
|
-
//
|
|
52
|
-
//
|
|
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
|
-
|
|
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 "
|
|
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 (
|
|
17
|
+
if (schema.length === 0)
|
|
19
18
|
return "empty";
|
|
20
|
-
if (
|
|
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 = () => ({
|
|
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({
|
|
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, {
|
|
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,
|
|
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);
|
package/docs/AGENT_BEHAVIOR.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
472
|
-
|
|
473
|
-
|
|
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
|
|