@zktx.io/sui-mcp 0.5.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/LICENSE +21 -0
- package/LICENSES/@mysten-bcs-2.0.5-Apache-2.0.txt +205 -0
- package/LICENSES/@mysten-sui-2.17.0-Apache-2.0.txt +201 -0
- package/LICENSES/@mysten-utils-0.3.3-Apache-2.0.txt +205 -0
- package/LICENSES/fancy-canvas-2.1.0-MIT.txt +7 -0
- package/LICENSES/lightweight-charts-5.2.0-NOTICE.txt +2 -0
- package/README.md +288 -0
- package/dist/adapters/adapterLifecycleValidators.js +5 -0
- package/dist/adapters/adapterPromptSurfaces.js +60 -0
- package/dist/adapters/deepbook/deepbookHumanReviewProducer.js +178 -0
- package/dist/adapters/deepbook/deepbookQuotePolicy.js +112 -0
- package/dist/adapters/deepbook/deepbookReviewEvidence.js +507 -0
- package/dist/adapters/deepbook/deepbookReviewLifecycle.js +85 -0
- package/dist/adapters/deepbook/deepbookSwapIntent.js +79 -0
- package/dist/adapters/deepbook/deepbookTransactionMaterialProducer.js +254 -0
- package/dist/adapters/intentPlanFactories.js +51 -0
- package/dist/adapters/reviewAdapters.js +47 -0
- package/dist/core/action/adapterLifecycleValidation.js +12 -0
- package/dist/core/action/contractNameRegistry.js +151 -0
- package/dist/core/action/forbiddenFields.js +43 -0
- package/dist/core/action/humanReadableReviewEvidence.js +193 -0
- package/dist/core/action/humanReadableReviewProjectionVerifier.js +29 -0
- package/dist/core/action/ptbVisualizationProducer.js +71 -0
- package/dist/core/action/receiptPtbGraph.js +29 -0
- package/dist/core/action/reviewCheckResults.js +6 -0
- package/dist/core/action/reviewStateValidation.js +11 -0
- package/dist/core/action/reviewTimeSimulationEvidence.js +470 -0
- package/dist/core/action/schemas.js +491 -0
- package/dist/core/action/signableAdapterContract.js +998 -0
- package/dist/core/action/suiChainReceiptEvidence.js +106 -0
- package/dist/core/action/suiChainReceiptMapping.js +107 -0
- package/dist/core/action/suiChainReceiptReader.js +211 -0
- package/dist/core/action/suiChainReceiptVerifier.js +178 -0
- package/dist/core/action/swapHumanReadableReviewProjection.js +130 -0
- package/dist/core/action/swapQuotePolicyEvidence.js +271 -0
- package/dist/core/action/transactionFunding.js +244 -0
- package/dist/core/action/transactionObjectOwnershipEvidence.js +256 -0
- package/dist/core/action/transactionObjectOwnershipProducer.js +323 -0
- package/dist/core/action/types.js +33 -0
- package/dist/core/action/walletReviewContractAssembler.js +286 -0
- package/dist/core/activity/accountAssetTimeline.js +157 -0
- package/dist/core/activity/accountAssetTimelineUsdcReferences.js +120 -0
- package/dist/core/activity/activityStore.js +16 -0
- package/dist/core/activity/externalActivityCoverage.js +76 -0
- package/dist/core/activity/externalActivityTransactionStream.js +82 -0
- package/dist/core/activity/localDataService.js +296 -0
- package/dist/core/activity/localDataTypes.js +11 -0
- package/dist/core/activity/localDataValidation.js +439 -0
- package/dist/core/activity/schemaVersion.js +2 -0
- package/dist/core/activity/sqliteActivityStore.js +1068 -0
- package/dist/core/activity/sqliteActivityStoreRows.js +389 -0
- package/dist/core/activity/sqliteActivityStoreSchema.js +367 -0
- package/dist/core/activity/sqliteActivityStoreTypes.js +5 -0
- package/dist/core/activity/suiFunctionTarget.js +43 -0
- package/dist/core/activity/transactionActivityAccountEffects.js +186 -0
- package/dist/core/activity/transactionActivityAnalysis.js +295 -0
- package/dist/core/activity/transactionActivityClassifier.js +306 -0
- package/dist/core/activity/transactionActivityDetails.js +229 -0
- package/dist/core/activity/transactionActivityProtocolRules.js +218 -0
- package/dist/core/activity/transactionActivityScanPolicy.js +170 -0
- package/dist/core/activity/transactionActivityService.js +379 -0
- package/dist/core/activity/transactionActivityTypes.js +18 -0
- package/dist/core/eventlog/sink.js +35 -0
- package/dist/core/evidence/settlementFamilies.js +85 -0
- package/dist/core/evidence/userAnswerUse.js +1 -0
- package/dist/core/numeric/rawU64.js +63 -0
- package/dist/core/preferences/preferencesStore.js +26 -0
- package/dist/core/preferences/sqlitePreferencesRepository.js +136 -0
- package/dist/core/proposal/externalProposalReview.js +347 -0
- package/dist/core/proposal/schemas.js +208 -0
- package/dist/core/proposal/types.js +35 -0
- package/dist/core/read/amounts.js +14 -0
- package/dist/core/read/coinMetadata.js +60 -0
- package/dist/core/read/deepbookOfficialIndexerSource.js +373 -0
- package/dist/core/read/deepbookRawQuoteClient.js +86 -0
- package/dist/core/read/deepbookReadHelpers.js +321 -0
- package/dist/core/read/deepbookRegistry.js +157 -0
- package/dist/core/read/deepbookSourceOwners.js +159 -0
- package/dist/core/read/deepbookUsdcChartService.js +342 -0
- package/dist/core/read/intentEvidenceResponseFormatting.js +228 -0
- package/dist/core/read/readCardInputs.js +19 -0
- package/dist/core/read/readCardResult.js +40 -0
- package/dist/core/read/readResponseGuidance.js +402 -0
- package/dist/core/read/readService.js +1639 -0
- package/dist/core/read/readServiceTypes.js +88 -0
- package/dist/core/read/settlementParityFormatting.js +82 -0
- package/dist/core/read/walletReadHelpers.js +99 -0
- package/dist/core/review/reviewChecks.js +45 -0
- package/dist/core/review/reviewComputation.js +38 -0
- package/dist/core/review/reviewComputationResult.js +87 -0
- package/dist/core/session/cardSession.js +31 -0
- package/dist/core/session/cardSessionStore.js +204 -0
- package/dist/core/session/keyedRecordStore.js +18 -0
- package/dist/core/session/liveReviewSessionContract.js +3 -0
- package/dist/core/session/localSession.js +31 -0
- package/dist/core/session/privateReviewArtifacts.js +75 -0
- package/dist/core/session/reviewValidity.js +49 -0
- package/dist/core/session/sessionErrors.js +9 -0
- package/dist/core/session/sessionRecordStore.js +40 -0
- package/dist/core/session/sessionStore.js +655 -0
- package/dist/core/session/settingsSession.js +1 -0
- package/dist/core/session/settingsSessions.js +45 -0
- package/dist/core/session/sqliteCardStore.js +88 -0
- package/dist/core/session/sqliteSessionStore.js +213 -0
- package/dist/core/session/sqliteTransactionMaterialStore.js +64 -0
- package/dist/core/session/sqliteWalletWorkflowStore.js +821 -0
- package/dist/core/session/status.js +60 -0
- package/dist/core/session/transactionMaterialStore.js +221 -0
- package/dist/core/session/transactionRequest.js +72 -0
- package/dist/core/session/wait.js +79 -0
- package/dist/core/session/walletConnection.js +92 -0
- package/dist/core/session/walletRuntime.js +38 -0
- package/dist/core/session/walletWorkflow.js +608 -0
- package/dist/core/session/workflowState.js +80 -0
- package/dist/core/session/workflowView.js +150 -0
- package/dist/core/suiAddress.js +18 -0
- package/dist/core/suiEndpoint.js +72 -0
- package/dist/mcp/activeAccountResponse.js +24 -0
- package/dist/mcp/prompts.js +146 -0
- package/dist/mcp/registerTool.js +19 -0
- package/dist/mcp/resources.js +72 -0
- package/dist/mcp/responseGuidance.js +326 -0
- package/dist/mcp/result.js +17 -0
- package/dist/mcp/schemas.js +11 -0
- package/dist/mcp/server.js +32 -0
- package/dist/mcp/serverInfo.js +131 -0
- package/dist/mcp/toolErrors.js +118 -0
- package/dist/mcp/toolNames.js +58 -0
- package/dist/mcp/tools/account/index.js +44 -0
- package/dist/mcp/tools/action/prepareSuiActionReview.js +126 -0
- package/dist/mcp/tools/read/commonSchemas.js +43 -0
- package/dist/mcp/tools/read/deepbookReadTools.js +686 -0
- package/dist/mcp/tools/read/index.js +14 -0
- package/dist/mcp/tools/read/readToolHelpers.js +97 -0
- package/dist/mcp/tools/read/reviewActivityTools.js +179 -0
- package/dist/mcp/tools/read/serverStatusTools.js +100 -0
- package/dist/mcp/tools/read/transactionActivityOutput.js +371 -0
- package/dist/mcp/tools/read/transactionActivityTools.js +858 -0
- package/dist/mcp/tools/read/walletReadTools.js +734 -0
- package/dist/mcp/tools/session/executionResultTools.js +44 -0
- package/dist/mcp/tools/session/index.js +8 -0
- package/dist/mcp/tools/session/shared.js +36 -0
- package/dist/mcp/tools/session/statusTools.js +69 -0
- package/dist/mcp/tools/session/walletConnectionTools.js +69 -0
- package/dist/mcp/tools/settings/index.js +66 -0
- package/dist/mcp-app/account.html +3133 -0
- package/dist/mcp-app/account.notices.txt +973 -0
- package/dist/mcp-app/chart.html +3354 -0
- package/dist/mcp-app/chart.notices.txt +1194 -0
- package/dist/mcp-app/connect.html +3179 -0
- package/dist/mcp-app/connect.notices.txt +1012 -0
- package/dist/mcp-app/receipt.html +8430 -0
- package/dist/mcp-app/receipt.notices.txt +3549 -0
- package/dist/mcp-app/review.html +8430 -0
- package/dist/mcp-app/review.notices.txt +3549 -0
- package/dist/mcp-ui/contracts.js +31 -0
- package/dist/mcp-ui/readCards.js +28 -0
- package/dist/mcp-ui/tools.js +134 -0
- package/dist/review-app/brand-dark.svg +1 -0
- package/dist/review-app/brand-light.svg +1 -0
- package/dist/review-app/favicon.svg +1 -0
- package/dist/review-app/settings.css +1 -0
- package/dist/review-app/settings.js +1 -0
- package/dist/review-app/ui.css +809 -0
- package/dist/review-server/assets.js +48 -0
- package/dist/review-server/html.js +45 -0
- package/dist/review-server/http.js +47 -0
- package/dist/review-server/middleware/hostOrigin.js +48 -0
- package/dist/review-server/middleware/reviewToken.js +7 -0
- package/dist/review-server/reviewServerPolicy.js +1 -0
- package/dist/review-server/server.js +72 -0
- package/dist/review-server/settingsApi.js +158 -0
- package/dist/runtime/application.js +203 -0
- package/dist/runtime/config.js +97 -0
- package/dist/runtime/localSettingsService.js +198 -0
- package/dist/runtime/logger.js +66 -0
- package/dist/runtime/reviewDependencies.js +58 -0
- package/dist/runtime/reviewServerAcquire.js +93 -0
- package/dist/runtime/runtimeTermination.js +60 -0
- package/dist/runtime/shared/authenticatedFetch.js +127 -0
- package/dist/runtime/shared/control.js +93 -0
- package/dist/runtime/shared/dataAccess.js +66 -0
- package/dist/runtime/shared/mcpHttp.js +84 -0
- package/dist/runtime/shared/ownerLease.js +50 -0
- package/dist/runtime/shared/server.js +98 -0
- package/dist/runtime/shared/stdio.js +113 -0
- package/dist/runtime/smokeMainnetRead.js +391 -0
- package/dist/runtime/smokeMainnetReadAssertions.js +306 -0
- package/dist/runtime/smokeReview.js +40 -0
- package/dist/runtime/start.js +90 -0
- package/dist/runtime/suiEndpoint.js +97 -0
- package/dist/runtime/suiTransactionGraphqlMapping.js +200 -0
- package/dist/runtime/suiTransactionGraphqlQueries.js +231 -0
- package/dist/runtime/suiTransactionGraphqlSource.js +148 -0
- package/dist/runtime/walletConnectConfig.js +3 -0
- package/dist/runtime/walletConnectStorage.js +99 -0
- package/dist/runtime/walletConnectTransport.js +128 -0
- package/dist/runtime/walletSdkChild.js +3 -0
- package/dist/runtime/walletSdkChildRuntime.js +160 -0
- package/dist/runtime/walletSdkIpc.js +26 -0
- package/dist/runtime/walletSdkProcess.js +358 -0
- package/docs/AGENT_BEHAVIOR.md +524 -0
- package/docs/AGENT_DEVELOPMENT_POLICY.md +815 -0
- package/docs/FRONTEND_POLICY.md +534 -0
- package/docs/LOCAL_DB_ARCHITECTURE.md +381 -0
- package/docs/MCP_SETUP.md +742 -0
- package/docs/MCP_TOOLS.md +1318 -0
- package/docs/SDK_API.md +206 -0
- package/docs/SIGNABLE_ADAPTER_CONTRACT.md +396 -0
- package/docs/TRANSACTION_ACTIVITY_LOG.md +194 -0
- package/docs/UTILITY_INDEX.md +275 -0
- package/docs/WALLET_CONNECTION.md +238 -0
- package/docs/golden-scenarios/BEHAVIOR_MATRIX.md +166 -0
- package/docs/golden-scenarios/INTENT_EVIDENCE_GOLDEN_ANSWERS.md +161 -0
- package/docs/golden-scenarios/INTENT_EVIDENCE_MATRIX.md +259 -0
- package/package.json +79 -0
- package/protocols/deepbook-margin.md +15 -0
- package/protocols/deepbook-v3.md +24 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Agent, request as httpRequest } from "node:http";
|
|
2
|
+
import { connect } from "node:net";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { MAX_JSON_BODY_BYTES } from "../../review-server/http.js";
|
|
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
|
+
async function authenticatedChannel(port, control, signal) {
|
|
10
|
+
signal?.throwIfAborted();
|
|
11
|
+
const socket = connect({ host: "127.0.0.1", port });
|
|
12
|
+
const agent = new Agent({ keepAlive: true, maxSockets: 1 });
|
|
13
|
+
agent.createConnection = (() => socket);
|
|
14
|
+
const close = () => { agent.destroy(); socket.destroy(); };
|
|
15
|
+
const abort = () => socket.destroy(new Error("Local server connection aborted."));
|
|
16
|
+
const timer = setTimeout(() => socket.destroy(new Error("Local server identity timed out.")), IDENTITY_TIMEOUT_MS);
|
|
17
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
18
|
+
try {
|
|
19
|
+
await new Promise((resolve, reject) => { socket.once("connect", resolve); socket.once("error", reject); });
|
|
20
|
+
const challenge = randomBytes(32).toString("base64url");
|
|
21
|
+
const identity = await new Promise((resolve, reject) => {
|
|
22
|
+
const request = httpRequest({ host: "127.0.0.1", port, path: IDENTITY_PATH, method: "GET", agent,
|
|
23
|
+
headers: { Host: `127.0.0.1:${port}`, [IDENTITY_CHALLENGE_HEADER]: challenge, Connection: "keep-alive" }
|
|
24
|
+
}, (response) => {
|
|
25
|
+
if (response.socket !== socket || response.statusCode !== 200 ||
|
|
26
|
+
response.headers["content-type"] !== "application/json; charset=utf-8" ||
|
|
27
|
+
response.headers["cache-control"] !== "no-store") {
|
|
28
|
+
response.destroy();
|
|
29
|
+
reject(new Error("Incompatible local server response."));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const chunks = [];
|
|
33
|
+
let size = 0;
|
|
34
|
+
response.on("data", (chunk) => {
|
|
35
|
+
size += chunk.length;
|
|
36
|
+
if (size > MAX_JSON_BODY_BYTES)
|
|
37
|
+
response.destroy(new Error("Local server identity is too large."));
|
|
38
|
+
else
|
|
39
|
+
chunks.push(chunk);
|
|
40
|
+
});
|
|
41
|
+
response.once("error", reject);
|
|
42
|
+
response.once("end", () => {
|
|
43
|
+
try {
|
|
44
|
+
resolve(verifyServerIdentity(JSON.parse(Buffer.concat(chunks).toString("utf8")), control, challenge));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
reject(new Error("Local server authentication or compatibility check failed."));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
request.once("error", reject);
|
|
52
|
+
request.end();
|
|
53
|
+
});
|
|
54
|
+
return { agent, socket, identity, close };
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
close();
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
signal?.removeEventListener("abort", abort);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export async function probeAuthenticatedServer(port, control) {
|
|
66
|
+
const channel = await authenticatedChannel(port, control);
|
|
67
|
+
try {
|
|
68
|
+
return channel.identity;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
channel.close();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function createAuthenticatedFetch(port, control, expectedInstanceId) {
|
|
75
|
+
return async (input, init) => {
|
|
76
|
+
const url = new URL(input);
|
|
77
|
+
if (url.origin !== `http://127.0.0.1:${port}` || url.pathname !== INTERNAL_MCP_PATH || url.search !== "") {
|
|
78
|
+
throw new Error("Only the internal MCP endpoint is allowed.");
|
|
79
|
+
}
|
|
80
|
+
const channel = await authenticatedChannel(port, control, init?.signal ?? undefined);
|
|
81
|
+
if (expectedInstanceId !== undefined && channel.identity.instanceId !== expectedInstanceId) {
|
|
82
|
+
channel.close();
|
|
83
|
+
throw new Error("Shared server changed before dispatch. The operation was not sent.");
|
|
84
|
+
}
|
|
85
|
+
const headers = new Headers(init?.headers);
|
|
86
|
+
headers.set("host", url.host);
|
|
87
|
+
headers.set("authorization", `Bearer ${control.key}`);
|
|
88
|
+
headers.set(SERVER_INSTANCE_HEADER, channel.identity.instanceId);
|
|
89
|
+
headers.set("connection", "close");
|
|
90
|
+
const body = init?.body;
|
|
91
|
+
if (body !== undefined && body !== null && typeof body !== "string") {
|
|
92
|
+
channel.close();
|
|
93
|
+
throw new Error("Invalid internal MCP body.");
|
|
94
|
+
}
|
|
95
|
+
if (typeof body === "string")
|
|
96
|
+
headers.set("content-length", String(Buffer.byteLength(body)));
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const request = httpRequest({ host: "127.0.0.1", port, path: INTERNAL_MCP_PATH,
|
|
99
|
+
method: init?.method ?? "GET", headers: Object.fromEntries(headers), agent: channel.agent,
|
|
100
|
+
...(init?.signal ? { signal: init.signal } : {})
|
|
101
|
+
}, (response) => {
|
|
102
|
+
if (response.socket !== channel.socket) {
|
|
103
|
+
channel.close();
|
|
104
|
+
reject(new Error("Local server socket changed."));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const responseHeaders = new Headers();
|
|
108
|
+
for (const [name, value] of Object.entries(response.headers)) {
|
|
109
|
+
if (value !== undefined)
|
|
110
|
+
responseHeaders.set(name, Array.isArray(value) ? value.join(", ") : value);
|
|
111
|
+
}
|
|
112
|
+
response.once("close", channel.close);
|
|
113
|
+
const status = response.statusCode ?? 500;
|
|
114
|
+
if ([204, 205, 304].includes(status)) {
|
|
115
|
+
response.resume();
|
|
116
|
+
resolve(new Response(null, { status, headers: responseHeaders }));
|
|
117
|
+
}
|
|
118
|
+
else
|
|
119
|
+
resolve(new Response(Readable.toWeb(response), { status, headers: responseHeaders }));
|
|
120
|
+
});
|
|
121
|
+
request.once("socket", (socket) => { if (socket !== channel.socket || socket.destroyed)
|
|
122
|
+
request.destroy(new Error("Authenticated socket unavailable.")); });
|
|
123
|
+
request.once("error", (error) => { channel.close(); reject(error); });
|
|
124
|
+
request.end(body);
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { link, lstat, mkdir, readFile, realpath, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
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;
|
|
8
|
+
export const IDENTITY_PATH = "/__runtime/identity";
|
|
9
|
+
export const INTERNAL_MCP_PATH = "/__runtime/mcp";
|
|
10
|
+
export const IDENTITY_CHALLENGE_HEADER = "x-sui-mcp-identity-challenge";
|
|
11
|
+
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
|
+
export const IDENTITY_TIMEOUT_MS = 1000;
|
|
14
|
+
const secretSchema = z.string().regex(/^[A-Za-z0-9_-]{43}$/);
|
|
15
|
+
export const serverIdentitySchema = z.object({
|
|
16
|
+
service: z.literal("sui-mcp"),
|
|
17
|
+
role: z.literal("shared-server"),
|
|
18
|
+
apiVersion: z.literal(INTERNAL_API_VERSION),
|
|
19
|
+
databaseId: z.string().regex(/^[0-9a-f]{64}$/),
|
|
20
|
+
configurationId: z.string().regex(/^[0-9a-f]{64}$/),
|
|
21
|
+
instanceId: z.string().uuid(),
|
|
22
|
+
pid: z.number().int().positive(),
|
|
23
|
+
challenge: secretSchema,
|
|
24
|
+
proof: secretSchema
|
|
25
|
+
}).strict();
|
|
26
|
+
export async function loadControlIdentity(databasePath, configuration) {
|
|
27
|
+
await mkdir(dirname(databasePath), { recursive: true, mode: 0o700 });
|
|
28
|
+
const directory = await realpath(dirname(databasePath));
|
|
29
|
+
const path = join(directory, "runtime-control.key");
|
|
30
|
+
const temporary = join(directory, `.runtime-control-${randomUUID()}.tmp`);
|
|
31
|
+
await writeFile(temporary, randomBytes(32).toString("base64url"), { flag: "wx", mode: 0o600 });
|
|
32
|
+
try {
|
|
33
|
+
try {
|
|
34
|
+
await link(temporary, path);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code !== "EEXIST")
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
await unlink(temporary);
|
|
43
|
+
}
|
|
44
|
+
const stat = await lstat(path);
|
|
45
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== "win32" &&
|
|
46
|
+
((stat.mode & 0o077) !== 0 || (process.getuid !== undefined && stat.uid !== process.getuid())))) {
|
|
47
|
+
throw new Error("The runtime control key must be a private file owned by the current OS user.");
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
key: secretSchema.parse(await readFile(path, "utf8")),
|
|
51
|
+
databaseId: createHash("sha256").update(join(directory, basename(databasePath))).digest("hex"),
|
|
52
|
+
configurationId: createHash("sha256").update(JSON.stringify(configuration)).digest("hex")
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function identityPayload(identity) {
|
|
56
|
+
return JSON.stringify([identity.service, identity.role, identity.apiVersion, identity.databaseId,
|
|
57
|
+
identity.configurationId, identity.instanceId, identity.pid, identity.challenge]);
|
|
58
|
+
}
|
|
59
|
+
export function createServerIdentity(control, instanceId, challenge) {
|
|
60
|
+
secretSchema.parse(challenge);
|
|
61
|
+
const identity = { service: "sui-mcp", role: "shared-server", apiVersion: INTERNAL_API_VERSION,
|
|
62
|
+
databaseId: control.databaseId, configurationId: control.configurationId,
|
|
63
|
+
instanceId, pid: process.pid, challenge };
|
|
64
|
+
return { ...identity, proof: createHmac("sha256", control.key).update(identityPayload(identity)).digest("base64url") };
|
|
65
|
+
}
|
|
66
|
+
export function verifyServerIdentity(input, control, challenge) {
|
|
67
|
+
const identity = serverIdentitySchema.parse(input);
|
|
68
|
+
if (identity.databaseId !== control.databaseId || identity.configurationId !== control.configurationId ||
|
|
69
|
+
identity.challenge !== challenge)
|
|
70
|
+
throw new Error("Local server identity does not match this runtime.");
|
|
71
|
+
const expected = createHmac("sha256", control.key).update(identityPayload(identity)).digest();
|
|
72
|
+
const actual = Buffer.from(identity.proof, "base64url");
|
|
73
|
+
if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
|
|
74
|
+
throw new Error("Local server authentication failed.");
|
|
75
|
+
}
|
|
76
|
+
return identity;
|
|
77
|
+
}
|
|
78
|
+
export function singleHeader(request, name) {
|
|
79
|
+
const values = request.headersDistinct[name];
|
|
80
|
+
return values?.length === 1 ? values[0] : undefined;
|
|
81
|
+
}
|
|
82
|
+
export function validInternalRequest(request, port) {
|
|
83
|
+
return singleHeader(request, "host") === `127.0.0.1:${port}` &&
|
|
84
|
+
request.headers.origin === undefined && request.headers.cookie === undefined;
|
|
85
|
+
}
|
|
86
|
+
export function validControlAuthorization(request, control, instanceId) {
|
|
87
|
+
const authorization = singleHeader(request, "authorization");
|
|
88
|
+
if (authorization === undefined || singleHeader(request, SERVER_INSTANCE_HEADER) !== instanceId)
|
|
89
|
+
return false;
|
|
90
|
+
const expected = Buffer.from(`Bearer ${control.key}`);
|
|
91
|
+
const actual = Buffer.from(authorization);
|
|
92
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
93
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
export class RuntimeDataAccess {
|
|
5
|
+
context = new AsyncLocalStorage();
|
|
6
|
+
generation = 0;
|
|
7
|
+
initializing = true;
|
|
8
|
+
closed = false;
|
|
9
|
+
run(work) {
|
|
10
|
+
if (this.closed)
|
|
11
|
+
throw new Error("Shared server is closed.");
|
|
12
|
+
return this.context.run({ generation: this.generation }, work);
|
|
13
|
+
}
|
|
14
|
+
ready() { this.initializing = false; }
|
|
15
|
+
bind(work) {
|
|
16
|
+
this.assertCurrent();
|
|
17
|
+
const generation = this.generation;
|
|
18
|
+
return (value) => this.context.run({ generation }, () => { this.assertCurrent(); work(value); });
|
|
19
|
+
}
|
|
20
|
+
assertCurrent = () => {
|
|
21
|
+
const context = this.context.getStore();
|
|
22
|
+
if (this.closed || (context === undefined ? !this.initializing : context.generation !== this.generation)) {
|
|
23
|
+
throw new Error("Shared data changed or its server stopped; stale access is refused.");
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
dataReplaced() {
|
|
27
|
+
this.assertCurrent();
|
|
28
|
+
this.generation += 1;
|
|
29
|
+
const active = this.context.getStore();
|
|
30
|
+
if (active !== undefined)
|
|
31
|
+
active.generation = this.generation;
|
|
32
|
+
}
|
|
33
|
+
close() { this.closed = true; }
|
|
34
|
+
guardDatabase = (database) => {
|
|
35
|
+
const guard = this.assertCurrent;
|
|
36
|
+
return new Proxy(database, {
|
|
37
|
+
get(target, property) {
|
|
38
|
+
if (property === "prepare")
|
|
39
|
+
return (sql) => {
|
|
40
|
+
guard();
|
|
41
|
+
const statement = target.prepare(sql);
|
|
42
|
+
return new Proxy(statement, {
|
|
43
|
+
get(stmt, key) {
|
|
44
|
+
const value = Reflect.get(stmt, key, stmt);
|
|
45
|
+
return typeof value === "function" ? (...args) => {
|
|
46
|
+
guard();
|
|
47
|
+
return Reflect.apply(value, stmt, args);
|
|
48
|
+
} : value;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
if (property === "transaction")
|
|
53
|
+
return (work) => {
|
|
54
|
+
guard();
|
|
55
|
+
return target.transaction((...args) => { guard(); return work(...args); });
|
|
56
|
+
};
|
|
57
|
+
const value = Reflect.get(target, property, target);
|
|
58
|
+
return typeof value === "function" ? (...args) => {
|
|
59
|
+
if (property !== "close")
|
|
60
|
+
guard();
|
|
61
|
+
return Reflect.apply(value, target, args);
|
|
62
|
+
} : value;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { CARD_TOOLS } from "../../mcp-ui/contracts.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { isJSONRPCRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { MAX_JSON_BODY_BYTES, sendJson } from "../../review-server/http.js";
|
|
6
|
+
export function createInternalMcpHandler(createServer) {
|
|
7
|
+
const sessions = new Map();
|
|
8
|
+
const connections = new Set();
|
|
9
|
+
return {
|
|
10
|
+
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
|
+
if (request.method === "GET") {
|
|
14
|
+
response.setHeader("Allow", "POST, DELETE");
|
|
15
|
+
sendJson(response, 405, { error: "method_not_allowed" });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const sessionId = request.headers["mcp-session-id"];
|
|
19
|
+
if (sessionId !== undefined && typeof sessionId !== "string") {
|
|
20
|
+
sendJson(response, 400, { error: "invalid_session" });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (sessionId !== undefined) {
|
|
24
|
+
const session = sessions.get(sessionId);
|
|
25
|
+
if (!session) {
|
|
26
|
+
sendJson(response, 404, { error: "mcp_session_unavailable" });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await session.transport.handleRequest(request, response);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (request.method !== "POST") {
|
|
33
|
+
sendJson(response, 400, { error: "mcp_initialize_required" });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const server = createServer();
|
|
37
|
+
const transport = new StreamableHTTPServerTransport({
|
|
38
|
+
sessionIdGenerator: randomUUID,
|
|
39
|
+
enableJsonResponse: true,
|
|
40
|
+
onsessioninitialized: (id) => { sessions.set(id, { server, transport }); },
|
|
41
|
+
onsessionclosed: (id) => { sessions.delete(id); connections.delete(server); }
|
|
42
|
+
});
|
|
43
|
+
connections.add(server);
|
|
44
|
+
try {
|
|
45
|
+
const connection = {
|
|
46
|
+
start: () => transport.start(),
|
|
47
|
+
send: (message, options) => transport.send(message, options),
|
|
48
|
+
close: () => transport.close()
|
|
49
|
+
};
|
|
50
|
+
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
|
+
if (isJSONRPCRequest(message) && message.method === "tools/call" &&
|
|
54
|
+
Object.values(CARD_TOOLS).some((name) => name === message.params?.name) &&
|
|
55
|
+
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." } });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
connection.onmessage?.(message, extra);
|
|
60
|
+
};
|
|
61
|
+
transport.onerror = (error) => connection.onerror?.(error);
|
|
62
|
+
transport.onclose = () => connection.onclose?.();
|
|
63
|
+
await server.connect(connection);
|
|
64
|
+
await transport.handleRequest(request, response);
|
|
65
|
+
if (transport.sessionId === undefined) {
|
|
66
|
+
connections.delete(server);
|
|
67
|
+
await server.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (transport.sessionId !== undefined)
|
|
72
|
+
sessions.delete(transport.sessionId);
|
|
73
|
+
connections.delete(server);
|
|
74
|
+
await server.close();
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
async close() {
|
|
79
|
+
await Promise.all([...connections].map((server) => server.close()));
|
|
80
|
+
connections.clear();
|
|
81
|
+
sessions.clear();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
export class DataDirectoryOwnedError extends Error {
|
|
5
|
+
code = "DATA_DIRECTORY_OWNED";
|
|
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
|
+
export function acquireDataDirectoryOwner(databasePath) {
|
|
10
|
+
const directory = dirname(databasePath);
|
|
11
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
12
|
+
const directoryStat = statSync(directory);
|
|
13
|
+
if (!directoryStat.isDirectory() || (process.platform !== "win32" && ((directoryStat.mode & 0o077) !== 0 ||
|
|
14
|
+
(process.getuid !== undefined && directoryStat.uid !== process.getuid())))) {
|
|
15
|
+
throw new Error("The runtime data directory must be private to the current OS user.");
|
|
16
|
+
}
|
|
17
|
+
const filename = join(directory, "runtime-owner.sqlite");
|
|
18
|
+
if (existsSync(filename)) {
|
|
19
|
+
const stat = lstatSync(filename);
|
|
20
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== "win32" &&
|
|
21
|
+
((stat.mode & 0o077) !== 0 || (process.getuid !== undefined && stat.uid !== process.getuid())))) {
|
|
22
|
+
throw new Error("Runtime ownership file is not private to this OS user.");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const mask = process.umask(0o077);
|
|
26
|
+
let lock;
|
|
27
|
+
try {
|
|
28
|
+
lock = new Database(filename, { timeout: 0 });
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
process.umask(mask);
|
|
32
|
+
}
|
|
33
|
+
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
|
+
lock.pragma("locking_mode=EXCLUSIVE");
|
|
37
|
+
lock.exec("BEGIN EXCLUSIVE");
|
|
38
|
+
lock.exec("CREATE TABLE IF NOT EXISTS owner_lock (singleton INTEGER PRIMARY KEY CHECK (singleton=1))");
|
|
39
|
+
lock.exec("COMMIT");
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
lock.close();
|
|
43
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "SQLITE_BUSY") {
|
|
44
|
+
throw new DataDirectoryOwnedError("This data directory already has a runtime owner. Use the same port for its clients, or a different data directory.");
|
|
45
|
+
}
|
|
46
|
+
throw new Error("The runtime data directory could not be locked.");
|
|
47
|
+
}
|
|
48
|
+
return { close: () => { if (lock.open)
|
|
49
|
+
lock.close(); } };
|
|
50
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { HttpError, sendJson } from "../../review-server/http.js";
|
|
4
|
+
import { createServerIdentity, IDENTITY_CHALLENGE_HEADER, IDENTITY_PATH, INTERNAL_MCP_PATH, singleHeader, validControlAuthorization, validInternalRequest } from "./control.js";
|
|
5
|
+
export async function startSharedServer(input) {
|
|
6
|
+
const instanceId = randomUUID();
|
|
7
|
+
let port = input.port;
|
|
8
|
+
let application;
|
|
9
|
+
let stopping = false;
|
|
10
|
+
const server = createServer((request, response) => {
|
|
11
|
+
response.setHeader("cache-control", "no-store");
|
|
12
|
+
void (async () => {
|
|
13
|
+
try {
|
|
14
|
+
if (stopping) {
|
|
15
|
+
sendJson(response, 503, { error: "runtime_stopping" });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (request.url === IDENTITY_PATH || request.url === INTERNAL_MCP_PATH) {
|
|
19
|
+
if (!validInternalRequest(request, port)) {
|
|
20
|
+
sendJson(response, 403, { error: "invalid_request_origin" });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (request.url === IDENTITY_PATH) {
|
|
24
|
+
if (request.method !== "GET" || request.headers.authorization !== undefined) {
|
|
25
|
+
sendJson(response, 400, { error: "invalid_identity_request" });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const challenge = singleHeader(request, IDENTITY_CHALLENGE_HEADER);
|
|
29
|
+
if (challenge === undefined || !/^[A-Za-z0-9_-]{43}$/.test(challenge)) {
|
|
30
|
+
sendJson(response, 400, { error: "invalid_identity_challenge" });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
sendJson(response, 200, createServerIdentity(input.control, instanceId, challenge));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!validControlAuthorization(request, input.control, instanceId)) {
|
|
37
|
+
sendJson(response, 401, { error: "control_authentication_required" });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
await (await application).handleMcp(request, response);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
await (await application).handleHttp(request, response);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error instanceof HttpError) {
|
|
47
|
+
if (!response.headersSent)
|
|
48
|
+
sendJson(response, error.status, { error: error.code });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
input.onError(error);
|
|
52
|
+
if (!response.headersSent)
|
|
53
|
+
sendJson(response, 503, { error: "runtime_unavailable" });
|
|
54
|
+
else
|
|
55
|
+
response.destroy();
|
|
56
|
+
}
|
|
57
|
+
})();
|
|
58
|
+
});
|
|
59
|
+
await new Promise((resolve, reject) => {
|
|
60
|
+
const failed = (error) => { server.off("listening", listening); reject(error); };
|
|
61
|
+
const listening = () => { server.off("error", failed); resolve(); };
|
|
62
|
+
server.once("error", failed);
|
|
63
|
+
server.once("listening", listening);
|
|
64
|
+
server.listen(port, "127.0.0.1");
|
|
65
|
+
});
|
|
66
|
+
port = server.address().port;
|
|
67
|
+
// Only the process that owns the port opens SQLite or initializes services.
|
|
68
|
+
application = Promise.resolve().then(() => input.createApplication(instanceId));
|
|
69
|
+
const closeListener = async () => {
|
|
70
|
+
if (!server.listening)
|
|
71
|
+
return;
|
|
72
|
+
const closed = new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
73
|
+
server.closeAllConnections();
|
|
74
|
+
await closed;
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
await application;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
stopping = true;
|
|
81
|
+
await closeListener();
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
host: "127.0.0.1", port, instanceId,
|
|
86
|
+
async close() {
|
|
87
|
+
if (stopping)
|
|
88
|
+
return;
|
|
89
|
+
stopping = true;
|
|
90
|
+
try {
|
|
91
|
+
await (await application).close();
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
await closeListener();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
+
import { CallToolRequestSchema, CallToolResultSchema, CompleteRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { SERVER_INSTRUCTIONS, SERVER_NAME, SERVER_VERSION } from "../../mcp/serverInfo.js";
|
|
6
|
+
import { createAuthenticatedFetch, probeAuthenticatedServer } from "./authenticatedFetch.js";
|
|
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
|
+
export async function startSharedStdio(input) {
|
|
12
|
+
const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, {
|
|
13
|
+
capabilities: { tools: {}, resources: {}, prompts: {}, completions: {} }, instructions: SERVER_INSTRUCTIONS
|
|
14
|
+
});
|
|
15
|
+
let connection;
|
|
16
|
+
let connecting;
|
|
17
|
+
let closed = false;
|
|
18
|
+
const lifetime = new AbortController();
|
|
19
|
+
function waiting(work, signal) {
|
|
20
|
+
if (signal.aborted)
|
|
21
|
+
return Promise.reject(signal.reason);
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const abort = () => reject(new Error("MCP connection was closed or cancelled."));
|
|
24
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
25
|
+
void work.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async function owner(signal) {
|
|
29
|
+
if (closed)
|
|
30
|
+
throw new Error("MCP connection is closed.");
|
|
31
|
+
signal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
|
|
32
|
+
const [control] = await waiting(Promise.all([input.control, input.ready]), signal);
|
|
33
|
+
if (connecting)
|
|
34
|
+
return connecting;
|
|
35
|
+
const identity = await probeAuthenticatedServer(input.port, control);
|
|
36
|
+
signal?.throwIfAborted();
|
|
37
|
+
if (connection?.instanceId === identity.instanceId)
|
|
38
|
+
return connection.client;
|
|
39
|
+
if (connecting)
|
|
40
|
+
return connecting;
|
|
41
|
+
const open = (async () => {
|
|
42
|
+
await connection?.client.close();
|
|
43
|
+
connection = undefined;
|
|
44
|
+
if (closed)
|
|
45
|
+
throw new Error("MCP connection is closed.");
|
|
46
|
+
const clientInfo = server.getClientVersion();
|
|
47
|
+
if (!clientInfo)
|
|
48
|
+
throw new Error("MCP client initialization is required.");
|
|
49
|
+
const client = new Client(clientInfo, { capabilities: server.getClientCapabilities() ?? {} });
|
|
50
|
+
const http = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${input.port}${INTERNAL_MCP_PATH}`), {
|
|
51
|
+
fetch: createAuthenticatedFetch(input.port, control, identity.instanceId)
|
|
52
|
+
});
|
|
53
|
+
const transport = {
|
|
54
|
+
start: () => http.start(), send: (message, options) => http.send(message, options),
|
|
55
|
+
setProtocolVersion: (version) => http.setProtocolVersion(version),
|
|
56
|
+
close: async () => { try {
|
|
57
|
+
await http.terminateSession();
|
|
58
|
+
}
|
|
59
|
+
catch { /* The prior owner may already be gone. */ } await http.close(); }
|
|
60
|
+
};
|
|
61
|
+
http.onmessage = (message) => { if (http.sessionId !== undefined)
|
|
62
|
+
transport.sessionId = http.sessionId; transport.onmessage?.(message); };
|
|
63
|
+
http.onerror = (error) => transport.onerror?.(error);
|
|
64
|
+
http.onclose = () => transport.onclose?.();
|
|
65
|
+
try {
|
|
66
|
+
await client.connect(transport, signal === undefined ? undefined : { signal });
|
|
67
|
+
if (closed) {
|
|
68
|
+
await client.close();
|
|
69
|
+
throw new Error("MCP connection is closed.");
|
|
70
|
+
}
|
|
71
|
+
connection = { instanceId: identity.instanceId, client };
|
|
72
|
+
return client;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
await client.close();
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
})();
|
|
79
|
+
connecting = open;
|
|
80
|
+
try {
|
|
81
|
+
return await open;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
if (connecting === open)
|
|
85
|
+
connecting = undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
server.setRequestHandler(ListToolsRequestSchema, async (request, extra) => (await owner(extra.signal)).listTools(request.params, { signal: extra.signal }));
|
|
89
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => (await owner(extra.signal)).callTool(request.params, CallToolResultSchema, { signal: extra.signal }));
|
|
90
|
+
server.setRequestHandler(ListResourcesRequestSchema, async (request, extra) => (await owner(extra.signal)).listResources(request.params, { signal: extra.signal }));
|
|
91
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async (request, extra) => (await owner(extra.signal)).listResourceTemplates(request.params, { signal: extra.signal }));
|
|
92
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => (await owner(extra.signal)).readResource(request.params, { signal: extra.signal }));
|
|
93
|
+
server.setRequestHandler(ListPromptsRequestSchema, async (request, extra) => (await owner(extra.signal)).listPrompts(request.params, { signal: extra.signal }));
|
|
94
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request, extra) => (await owner(extra.signal)).getPrompt(request.params, { signal: extra.signal }));
|
|
95
|
+
server.setRequestHandler(CompleteRequestSchema, async (request, extra) => (await owner(extra.signal)).complete(request.params, { signal: extra.signal }));
|
|
96
|
+
server.onerror = input.onError;
|
|
97
|
+
await server.connect(input.stdio);
|
|
98
|
+
return {
|
|
99
|
+
async close() {
|
|
100
|
+
if (closed)
|
|
101
|
+
return;
|
|
102
|
+
closed = true;
|
|
103
|
+
lifetime.abort();
|
|
104
|
+
await server.close();
|
|
105
|
+
try {
|
|
106
|
+
await connecting;
|
|
107
|
+
}
|
|
108
|
+
catch { /* Initialization was cancelled with the stdio connection. */ }
|
|
109
|
+
await connection?.client.close();
|
|
110
|
+
connection = undefined;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|