taurusdb-mcp 0.2.0 → 0.4.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 +12 -2
- package/dist/commands/credentials.d.ts +3 -0
- package/dist/commands/credentials.js +190 -0
- package/dist/index.js +5 -0
- package/dist/server.d.ts +1 -2
- package/dist/server.js +16 -20
- package/dist/tools/error-handling.js +3 -3
- package/dist/tools/query.js +8 -8
- package/dist/tools/registry.d.ts +2 -7
- package/dist/tools/registry.js +5 -18
- package/dist/tools/taurus/cloud-context.d.ts +0 -3
- package/dist/tools/taurus/cloud-context.js +4 -156
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -4
- package/scripts/windows-credential-configure.ps1 +51 -0
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ claude mcp add huaweicloud-taurusdb \
|
|
|
38
38
|
-e TAURUSDB_CLOUD_SECRET_ACCESS_KEY=<your-sk> \
|
|
39
39
|
-e TAURUSDB_SQL_DATABASE=<your-database> \
|
|
40
40
|
-e TAURUSDB_SQL_USER=<your-readonly-user> \
|
|
41
|
-
-e TAURUSDB_SQL_PASSWORD
|
|
41
|
+
-e TAURUSDB_SQL_PASSWORD='hw-kms-file:~/.taurusdb-mcp/password.ciphertext' \
|
|
42
42
|
-- npx -y taurusdb-mcp
|
|
43
43
|
```
|
|
44
44
|
|
|
@@ -51,7 +51,7 @@ codex mcp add huaweicloud-taurusdb \
|
|
|
51
51
|
--env TAURUSDB_CLOUD_SECRET_ACCESS_KEY=<your-sk> \
|
|
52
52
|
--env TAURUSDB_SQL_DATABASE=<your-database> \
|
|
53
53
|
--env TAURUSDB_SQL_USER=<your-readonly-user> \
|
|
54
|
-
--env TAURUSDB_SQL_PASSWORD
|
|
54
|
+
--env TAURUSDB_SQL_PASSWORD='hw-kms-file:~/.taurusdb-mcp/password.ciphertext' \
|
|
55
55
|
-- npx -y taurusdb-mcp
|
|
56
56
|
```
|
|
57
57
|
|
|
@@ -63,7 +63,17 @@ npx taurusdb-mcp init --client cursor
|
|
|
63
63
|
npx taurusdb-mcp init --client vscode
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
Configure and verify the operating-system credential store:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx taurusdb-mcp credentials configure
|
|
70
|
+
npx taurusdb-mcp credentials check
|
|
71
|
+
```
|
|
72
|
+
|
|
66
73
|
## Notes
|
|
67
74
|
|
|
68
75
|
- Requires Node.js `>= 20`
|
|
69
76
|
- Depends on `taurusdb-core`
|
|
77
|
+
- Database passwords support `env:`, `file:`, `hw-csms:`, `hw-kms:`, and `hw-kms-file:` references
|
|
78
|
+
- Huawei DEW CSMS is recommended when the database password should be stored and retrieved from Huawei Cloud
|
|
79
|
+
- macOS Keychain, Linux Secret Service, or Windows Credential Manager cloud identity is enabled with `TAURUSDB_CLOUD_KEYCHAIN_SERVICE`
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { canAuthenticateHuaweiCloudRequests, createConfigFromEnv, createHuaweiCsmsSecretResolver, createHuaweiKmsSecretResolver, createSqlProfileLoader, getHuaweiCloudAuthFromConfig, resolveHuaweiCloudProjectId, } from "taurusdb-core";
|
|
5
|
+
function readOption(args, name, fallback) {
|
|
6
|
+
const index = args.indexOf(name);
|
|
7
|
+
if (index === -1) {
|
|
8
|
+
return fallback;
|
|
9
|
+
}
|
|
10
|
+
const value = args[index + 1];
|
|
11
|
+
if (!value || value.startsWith("--")) {
|
|
12
|
+
throw new Error(`${name} requires a value.`);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function configureUsage() {
|
|
17
|
+
return `Usage: taurusdb-mcp credentials configure [options]
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--service <name> Credential service prefix (default: taurusdb-mcp/huaweicloud)
|
|
21
|
+
--account <name> Credential account (default: default)
|
|
22
|
+
--with-security-token Also store a temporary security token`;
|
|
23
|
+
}
|
|
24
|
+
function checkUsage() {
|
|
25
|
+
return `Usage: taurusdb-mcp credentials check
|
|
26
|
+
|
|
27
|
+
Checks the configured Huawei Cloud identity and cloud-backed database password
|
|
28
|
+
references without printing or connecting with any secret values.`;
|
|
29
|
+
}
|
|
30
|
+
function runCommand(command, args, errorMessage) {
|
|
31
|
+
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
32
|
+
if (result.error && "code" in result.error && result.error.code === "ENOENT") {
|
|
33
|
+
throw new Error(`${command} was not found.`);
|
|
34
|
+
}
|
|
35
|
+
if (result.status !== 0) {
|
|
36
|
+
throw new Error(errorMessage);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function storeMacOsSecret(service, account, label) {
|
|
40
|
+
process.stderr.write(`Enter ${label} in the macOS Keychain prompt.\n`);
|
|
41
|
+
runCommand("security", ["add-generic-password", "-U", "-s", service, "-a", account, "-w"], `Failed to store ${label} in macOS Keychain.`);
|
|
42
|
+
}
|
|
43
|
+
function storeLinuxSecret(service, account, key, label) {
|
|
44
|
+
process.stderr.write(`Enter ${label} when prompted by Linux Secret Service.\n`);
|
|
45
|
+
runCommand("secret-tool", [
|
|
46
|
+
"store",
|
|
47
|
+
`--label=TaurusDB MCP Huawei Cloud ${label}`,
|
|
48
|
+
"service",
|
|
49
|
+
service,
|
|
50
|
+
"account",
|
|
51
|
+
account,
|
|
52
|
+
"key",
|
|
53
|
+
key,
|
|
54
|
+
], `Failed to store ${label} in Linux Secret Service.`);
|
|
55
|
+
}
|
|
56
|
+
function configureWindows(service, account, withSecurityToken) {
|
|
57
|
+
const scriptPath = fileURLToPath(new URL("../../scripts/windows-credential-configure.ps1", import.meta.url));
|
|
58
|
+
const args = [
|
|
59
|
+
"-NoLogo",
|
|
60
|
+
"-NoProfile",
|
|
61
|
+
"-ExecutionPolicy",
|
|
62
|
+
"Bypass",
|
|
63
|
+
"-File",
|
|
64
|
+
path.normalize(scriptPath),
|
|
65
|
+
"-Service",
|
|
66
|
+
service,
|
|
67
|
+
"-Account",
|
|
68
|
+
account,
|
|
69
|
+
];
|
|
70
|
+
if (withSecurityToken) {
|
|
71
|
+
args.push("-WithSecurityToken");
|
|
72
|
+
}
|
|
73
|
+
runCommand("powershell.exe", args, "Failed to store credentials in Windows Credential Manager.");
|
|
74
|
+
}
|
|
75
|
+
export function runCredentialsConfigure(args) {
|
|
76
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
77
|
+
process.stdout.write(`${configureUsage()}\n`);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
const service = readOption(args, "--service", "taurusdb-mcp/huaweicloud");
|
|
81
|
+
const account = readOption(args, "--account", "default");
|
|
82
|
+
const withSecurityToken = args.includes("--with-security-token");
|
|
83
|
+
if (process.platform === "darwin") {
|
|
84
|
+
storeMacOsSecret(`${service}/access-key-id`, account, "Huawei Cloud access key ID");
|
|
85
|
+
storeMacOsSecret(`${service}/secret-access-key`, account, "Huawei Cloud secret access key");
|
|
86
|
+
if (withSecurityToken) {
|
|
87
|
+
storeMacOsSecret(`${service}/security-token`, account, "Huawei Cloud security token");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (process.platform === "linux") {
|
|
91
|
+
storeLinuxSecret(service, account, "access-key-id", "Huawei Cloud access key ID");
|
|
92
|
+
storeLinuxSecret(service, account, "secret-access-key", "Huawei Cloud secret access key");
|
|
93
|
+
if (withSecurityToken) {
|
|
94
|
+
storeLinuxSecret(service, account, "security-token", "Huawei Cloud security token");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else if (process.platform === "win32") {
|
|
98
|
+
configureWindows(service, account, withSecurityToken);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
throw new Error(`System credential storage is not supported on platform "${process.platform}".`);
|
|
102
|
+
}
|
|
103
|
+
process.stdout.write(`Stored Huawei Cloud credentials in service "${service}" for account "${account}".\n`);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
function printResult(ok, title, details) {
|
|
107
|
+
process.stdout.write(`${ok ? "[ok]" : "[fail]"} ${title}${details ? `: ${details}` : ""}\n`);
|
|
108
|
+
}
|
|
109
|
+
async function check(title, action) {
|
|
110
|
+
try {
|
|
111
|
+
printResult(true, title, await action());
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
printResult(false, title, error instanceof Error ? error.message : String(error));
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function getCloudResolver(uri, config) {
|
|
120
|
+
const normalized = uri.startsWith("uri:") ? uri.slice("uri:".length) : uri;
|
|
121
|
+
if (normalized.startsWith("hw-csms:")) {
|
|
122
|
+
return { kind: "CSMS", resolve: () => createHuaweiCsmsSecretResolver({ config })(normalized) };
|
|
123
|
+
}
|
|
124
|
+
if (normalized.startsWith("hw-kms:") || normalized.startsWith("hw-kms-file:")) {
|
|
125
|
+
return { kind: "KMS", resolve: () => createHuaweiKmsSecretResolver({ config })(normalized) };
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
export async function runCredentialsCheck(args) {
|
|
130
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
131
|
+
process.stdout.write(`${checkUsage()}\n`);
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
const config = createConfigFromEnv(process.env);
|
|
135
|
+
const auth = getHuaweiCloudAuthFromConfig(config);
|
|
136
|
+
let failed = false;
|
|
137
|
+
failed ||= !(await check("Huawei Cloud identity source", async () => {
|
|
138
|
+
if (auth.authToken)
|
|
139
|
+
return "IAM token configured";
|
|
140
|
+
if (auth.accessKeyId && auth.secretAccessKey)
|
|
141
|
+
return "AK/SK configured in environment";
|
|
142
|
+
if (auth.credentialProvider) {
|
|
143
|
+
await auth.credentialProvider();
|
|
144
|
+
return "system credential store readable";
|
|
145
|
+
}
|
|
146
|
+
if (!canAuthenticateHuaweiCloudRequests(auth)) {
|
|
147
|
+
throw new Error("No IAM token, AK/SK, or system credential store is configured.");
|
|
148
|
+
}
|
|
149
|
+
return "configured";
|
|
150
|
+
}));
|
|
151
|
+
if (!failed) {
|
|
152
|
+
failed ||= !(await check("Huawei Cloud project", async () => {
|
|
153
|
+
const projectId = await resolveHuaweiCloudProjectId(auth);
|
|
154
|
+
if (!projectId) {
|
|
155
|
+
throw new Error("Project ID could not be resolved.");
|
|
156
|
+
}
|
|
157
|
+
return config.cloud.projectId ? "configured" : "resolved through IAM";
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
const profiles = await createSqlProfileLoader({ config }).load();
|
|
161
|
+
const cloudRefs = [...profiles.values()]
|
|
162
|
+
.map((profile) => profile.user?.password)
|
|
163
|
+
.filter((ref) => ref?.type === "uri")
|
|
164
|
+
.map((ref) => getCloudResolver(ref.uri, config))
|
|
165
|
+
.filter((resolver) => resolver !== undefined);
|
|
166
|
+
if (cloudRefs.length === 0) {
|
|
167
|
+
printResult(true, "Cloud-backed database password references", "none configured; skipped");
|
|
168
|
+
}
|
|
169
|
+
else if (!failed) {
|
|
170
|
+
for (const resolver of cloudRefs) {
|
|
171
|
+
const readable = await check(`${resolver.kind} database password reference`, async () => {
|
|
172
|
+
await resolver.resolve();
|
|
173
|
+
return "readable";
|
|
174
|
+
});
|
|
175
|
+
failed ||= !readable;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return failed ? 1 : 0;
|
|
179
|
+
}
|
|
180
|
+
export async function runCredentials(args) {
|
|
181
|
+
const command = args[0];
|
|
182
|
+
if (command === "configure") {
|
|
183
|
+
return runCredentialsConfigure(args.slice(1));
|
|
184
|
+
}
|
|
185
|
+
if (command === "check") {
|
|
186
|
+
return runCredentialsCheck(args.slice(1));
|
|
187
|
+
}
|
|
188
|
+
process.stderr.write("Usage: taurusdb-mcp credentials <configure|check>\n");
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runInit } from "./commands/init.js";
|
|
3
|
+
import { runCredentials } from "./commands/credentials.js";
|
|
3
4
|
import { startMcpServer } from "./server.js";
|
|
4
5
|
import { VERSION } from "./version.js";
|
|
5
6
|
async function main() {
|
|
@@ -9,6 +10,10 @@ async function main() {
|
|
|
9
10
|
process.exitCode = await runInit(args.slice(1));
|
|
10
11
|
return;
|
|
11
12
|
}
|
|
13
|
+
if (firstArg === "credentials") {
|
|
14
|
+
process.exitCode = await runCredentials(args.slice(1));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
12
17
|
if (firstArg === "--version" || firstArg === "-v") {
|
|
13
18
|
process.stdout.write(`${VERSION}\n`);
|
|
14
19
|
return;
|
package/dist/server.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { TaurusDBEngine, type
|
|
2
|
+
import { TaurusDBEngine, type Config, type RuntimeTargetProfileLoader } from "taurusdb-core";
|
|
3
3
|
export interface ServerDeps {
|
|
4
4
|
config: Config;
|
|
5
5
|
profileLoader: RuntimeTargetProfileLoader;
|
|
6
6
|
engine: TaurusDBEngine;
|
|
7
7
|
pingResponse: string;
|
|
8
|
-
startupProbe?: CapabilitySnapshot;
|
|
9
8
|
}
|
|
10
9
|
export declare function bootstrapDependencies(): Promise<ServerDeps>;
|
|
11
10
|
export declare function createServer(deps: ServerDeps): McpServer;
|
package/dist/server.js
CHANGED
|
@@ -8,29 +8,11 @@ export async function bootstrapDependencies() {
|
|
|
8
8
|
const config = getConfig();
|
|
9
9
|
const profileLoader = new RuntimeOverrideProfileLoader(createSqlProfileLoader({ config }));
|
|
10
10
|
const engine = await TaurusDBEngine.create({ config, profileLoader });
|
|
11
|
-
const defaultDatasource = await engine.getDefaultDataSource();
|
|
12
|
-
let startupProbe;
|
|
13
|
-
if (defaultDatasource) {
|
|
14
|
-
try {
|
|
15
|
-
const bootstrapContext = await engine.resolveContext({
|
|
16
|
-
datasource: defaultDatasource,
|
|
17
|
-
readonly: true,
|
|
18
|
-
}, "task_bootstrap_probe");
|
|
19
|
-
startupProbe = await engine.probeCapabilities(bootstrapContext);
|
|
20
|
-
}
|
|
21
|
-
catch (error) {
|
|
22
|
-
logger.warn({
|
|
23
|
-
err: error,
|
|
24
|
-
defaultDatasource,
|
|
25
|
-
}, "Capability probe failed during bootstrap; TaurusDB-specific tools will stay disabled");
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
11
|
return {
|
|
29
12
|
config,
|
|
30
13
|
profileLoader,
|
|
31
14
|
engine,
|
|
32
15
|
pingResponse: "pong",
|
|
33
|
-
startupProbe,
|
|
34
16
|
};
|
|
35
17
|
}
|
|
36
18
|
export function createServer(deps) {
|
|
@@ -38,7 +20,22 @@ export function createServer(deps) {
|
|
|
38
20
|
name: "huaweicloud-taurusdb",
|
|
39
21
|
version: VERSION,
|
|
40
22
|
});
|
|
41
|
-
|
|
23
|
+
let cleanupPromise;
|
|
24
|
+
const cleanup = () => {
|
|
25
|
+
cleanupPromise ??= deps.engine.close();
|
|
26
|
+
return cleanupPromise;
|
|
27
|
+
};
|
|
28
|
+
server.server.onclose = () => {
|
|
29
|
+
void cleanup().catch((error) => {
|
|
30
|
+
logger.error({ err: error }, "Failed to close MCP session dependencies");
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
const closeServer = server.close.bind(server);
|
|
34
|
+
server.close = async () => {
|
|
35
|
+
await closeServer();
|
|
36
|
+
await cleanup();
|
|
37
|
+
};
|
|
38
|
+
registerTools(server, deps, deps.config);
|
|
42
39
|
return server;
|
|
43
40
|
}
|
|
44
41
|
export async function startMcpServer() {
|
|
@@ -52,7 +49,6 @@ export async function startMcpServer() {
|
|
|
52
49
|
logger.info({
|
|
53
50
|
profileCount: datasources.length,
|
|
54
51
|
defaultDatasource: defaultDatasource ?? null,
|
|
55
|
-
taurusdbProbe: deps.startupProbe?.kernelInfo?.isTaurusDB ?? null,
|
|
56
52
|
}, "SQL profiles resolved");
|
|
57
53
|
logger.info({ server: "huaweicloud-taurusdb", version: VERSION }, "Starting MCP server");
|
|
58
54
|
await server.connect(new StdioServerTransport());
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConnectionPoolError, DatasourceResolutionError, FlashbackNoViewError, SchemaIntrospectionError, UnsupportedFeatureError, } from "taurusdb-core";
|
|
1
|
+
import { ConnectionPoolError, DatasourceResolutionError, FlashbackNoViewError, logger, SchemaIntrospectionError, UnsupportedFeatureError, } from "taurusdb-core";
|
|
2
2
|
import { ErrorCode, formatError, } from "../utils/formatter.js";
|
|
3
3
|
export class ToolInputError extends Error {
|
|
4
4
|
constructor(message) {
|
|
@@ -116,11 +116,11 @@ export function formatToolError(error, context) {
|
|
|
116
116
|
metadata: context.metadata,
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
|
+
logger.error({ err: error, action: context.action }, "Tool execution failed unexpectedly");
|
|
119
120
|
return formatError({
|
|
120
121
|
code: ErrorCode.CONNECTION_FAILED,
|
|
121
|
-
message:
|
|
122
|
+
message: `${context.action} failed unexpectedly.`,
|
|
122
123
|
summary: `${context.action} failed unexpectedly.`,
|
|
123
124
|
metadata: context.metadata,
|
|
124
|
-
details: detailsOf(error),
|
|
125
125
|
});
|
|
126
126
|
}
|
package/dist/tools/query.js
CHANGED
|
@@ -12,16 +12,16 @@ async function inspectSql(toolName, sql, ctx, deps) {
|
|
|
12
12
|
context: ctx,
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
-
async function ensureConfirmation(
|
|
15
|
+
async function ensureConfirmation(decision, ctx, deps, taskId, confirmationToken) {
|
|
16
16
|
if (!decision.requiresConfirmation) {
|
|
17
17
|
return undefined;
|
|
18
18
|
}
|
|
19
19
|
const responseMetadata = metadata(taskId, {
|
|
20
20
|
sql_hash: decision.sqlHash,
|
|
21
|
-
statement_type: statementTypeFromSql(
|
|
21
|
+
statement_type: statementTypeFromSql(decision.normalizedSql),
|
|
22
22
|
});
|
|
23
23
|
if (confirmationToken) {
|
|
24
|
-
const validation = await deps.engine.validateConfirmation(confirmationToken,
|
|
24
|
+
const validation = await deps.engine.validateConfirmation(confirmationToken, decision.normalizedSql, ctx);
|
|
25
25
|
if (validation.valid) {
|
|
26
26
|
return undefined;
|
|
27
27
|
}
|
|
@@ -76,11 +76,11 @@ export const executeReadonlySqlTool = {
|
|
|
76
76
|
},
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
|
-
const confirmationResponse = await ensureConfirmation(
|
|
79
|
+
const confirmationResponse = await ensureConfirmation(decision, ctx, deps, context.taskId, asOptionalString(input.confirmation_token, "confirmation_token"));
|
|
80
80
|
if (confirmationResponse) {
|
|
81
81
|
return confirmationResponse;
|
|
82
82
|
}
|
|
83
|
-
const result = await deps.engine.executeReadonly(
|
|
83
|
+
const result = await deps.engine.executeReadonly(decision.normalizedSql, ctx, {
|
|
84
84
|
timeoutMs: decision.runtimeLimits.timeoutMs,
|
|
85
85
|
maxRows: decision.runtimeLimits.maxRows,
|
|
86
86
|
maxColumns: decision.runtimeLimits.maxColumns,
|
|
@@ -133,7 +133,7 @@ export const explainSqlTool = {
|
|
|
133
133
|
},
|
|
134
134
|
});
|
|
135
135
|
}
|
|
136
|
-
const result = await deps.engine.explain(
|
|
136
|
+
const result = await deps.engine.explain(decision.normalizedSql, ctx);
|
|
137
137
|
return formatSuccess(toPublicExplainResult(result, decision), {
|
|
138
138
|
summary: decision.requiresConfirmation
|
|
139
139
|
? "Explain generated. Executing this SQL would require explicit confirmation."
|
|
@@ -184,11 +184,11 @@ export const executeSqlTool = {
|
|
|
184
184
|
},
|
|
185
185
|
});
|
|
186
186
|
}
|
|
187
|
-
const confirmationResponse = await ensureConfirmation(
|
|
187
|
+
const confirmationResponse = await ensureConfirmation(decision, ctx, deps, context.taskId, asOptionalString(input.confirmation_token, "confirmation_token"));
|
|
188
188
|
if (confirmationResponse) {
|
|
189
189
|
return confirmationResponse;
|
|
190
190
|
}
|
|
191
|
-
const result = await deps.engine.executeMutation(
|
|
191
|
+
const result = await deps.engine.executeMutation(decision.normalizedSql, ctx, {
|
|
192
192
|
timeoutMs: decision.runtimeLimits.timeoutMs,
|
|
193
193
|
});
|
|
194
194
|
return formatSuccess(toPublicMutationResult(result), {
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type Config } from "taurusdb-core";
|
|
3
3
|
import type { ServerDeps } from "../server.js";
|
|
4
4
|
import { type ToolResponse } from "../utils/formatter.js";
|
|
5
5
|
export type ToolDeps = ServerDeps;
|
|
6
|
-
export interface ToolRegistrationProbe {
|
|
7
|
-
kernelInfo?: KernelInfo;
|
|
8
|
-
features?: FeatureMatrix;
|
|
9
|
-
}
|
|
10
6
|
export type ToolInvokeContext = {
|
|
11
7
|
taskId: string;
|
|
12
8
|
};
|
|
@@ -15,9 +11,8 @@ export interface ToolDefinition<I extends Record<string, unknown> = Record<strin
|
|
|
15
11
|
description: string;
|
|
16
12
|
inputSchema: Record<string, unknown>;
|
|
17
13
|
handler: (input: I, deps: ToolDeps, context: ToolInvokeContext) => Promise<ToolResponse<O>>;
|
|
18
|
-
exposeWhen?: (config: Config) => boolean;
|
|
19
14
|
}
|
|
20
15
|
export declare const commonToolDefinitions: ToolDefinition[];
|
|
21
16
|
export declare const capabilityToolDefinitions: ToolDefinition[];
|
|
22
17
|
export declare const taurusToolDefinitions: ToolDefinition[];
|
|
23
|
-
export declare function registerTools(server: McpServer, deps: ToolDeps, config: Config,
|
|
18
|
+
export declare function registerTools(server: McpServer, deps: ToolDeps, config: Config, tools?: ToolDefinition[]): void;
|
package/dist/tools/registry.js
CHANGED
|
@@ -4,7 +4,7 @@ import { describeTableTool, listDatabasesTool, listTablesTool, } from "./discove
|
|
|
4
4
|
import { showProcesslistTool } from "./processlist.js";
|
|
5
5
|
import { pingTool } from "./ping.js";
|
|
6
6
|
import { getKernelInfoTool, listTaurusFeaturesTool } from "./taurus/capability.js";
|
|
7
|
-
import {
|
|
7
|
+
import { getSessionBindingTool, selectCloudTaurusInstanceTool, setCloudRegionTool, setDefaultDatabaseTool, } from "./taurus/cloud-context.js";
|
|
8
8
|
import { listCloudTaurusInstancesTool } from "./taurus/cloud-instances.js";
|
|
9
9
|
import { diagnosticToolDefinitions } from "./taurus/diagnostics.js";
|
|
10
10
|
import { explainSqlEnhancedTool } from "./taurus/explain.js";
|
|
@@ -12,10 +12,10 @@ import { flashbackQueryTool } from "./taurus/flashback.js";
|
|
|
12
12
|
import { listRecycleBinTool, restoreRecycleBinTableTool } from "./taurus/recycle-bin.js";
|
|
13
13
|
import { ErrorCode, formatError, toMcpToolResult, } from "../utils/formatter.js";
|
|
14
14
|
function formatUnhandledToolError(error, taskId) {
|
|
15
|
-
|
|
15
|
+
void error;
|
|
16
16
|
const response = formatError({
|
|
17
17
|
code: ErrorCode.CONNECTION_FAILED,
|
|
18
|
-
message,
|
|
18
|
+
message: "Tool execution failed unexpectedly.",
|
|
19
19
|
summary: "Tool execution failed unexpectedly.",
|
|
20
20
|
metadata: { task_id: taskId },
|
|
21
21
|
retryable: false,
|
|
@@ -67,10 +67,7 @@ export const capabilityToolDefinitions = [
|
|
|
67
67
|
getKernelInfoTool,
|
|
68
68
|
listTaurusFeaturesTool,
|
|
69
69
|
setCloudRegionTool,
|
|
70
|
-
setCloudAccessKeysTool,
|
|
71
70
|
getSessionBindingTool,
|
|
72
|
-
setSqlCredentialsTool,
|
|
73
|
-
clearSqlCredentialsTool,
|
|
74
71
|
setDefaultDatabaseTool,
|
|
75
72
|
listCloudTaurusInstancesTool,
|
|
76
73
|
selectCloudTaurusInstanceTool,
|
|
@@ -81,8 +78,7 @@ export const taurusToolDefinitions = [
|
|
|
81
78
|
listRecycleBinTool,
|
|
82
79
|
restoreRecycleBinTableTool,
|
|
83
80
|
];
|
|
84
|
-
function buildDefaultToolDefinitions(_config
|
|
85
|
-
void probe;
|
|
81
|
+
function buildDefaultToolDefinitions(_config) {
|
|
86
82
|
return [
|
|
87
83
|
...commonToolDefinitions,
|
|
88
84
|
...capabilityToolDefinitions,
|
|
@@ -90,17 +86,8 @@ function buildDefaultToolDefinitions(_config, probe) {
|
|
|
90
86
|
...taurusToolDefinitions,
|
|
91
87
|
];
|
|
92
88
|
}
|
|
93
|
-
function
|
|
94
|
-
return Array.isArray(value);
|
|
95
|
-
}
|
|
96
|
-
export function registerTools(server, deps, config, probeOrTools, maybeTools) {
|
|
97
|
-
const probe = isToolDefinitionArray(probeOrTools) ? undefined : probeOrTools;
|
|
98
|
-
const tools = maybeTools
|
|
99
|
-
?? (isToolDefinitionArray(probeOrTools) ? probeOrTools : buildDefaultToolDefinitions(config, probe));
|
|
89
|
+
export function registerTools(server, deps, config, tools = buildDefaultToolDefinitions(config)) {
|
|
100
90
|
for (const tool of tools) {
|
|
101
|
-
if (tool.exposeWhen && !tool.exposeWhen(config)) {
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
91
|
registerOneTool(server, tool, deps);
|
|
105
92
|
}
|
|
106
93
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import type { ToolDefinition } from "../registry.js";
|
|
2
2
|
export declare const setCloudRegionTool: ToolDefinition;
|
|
3
|
-
export declare const setCloudAccessKeysTool: ToolDefinition;
|
|
4
3
|
export declare const setDefaultDatabaseTool: ToolDefinition;
|
|
5
|
-
export declare const setSqlCredentialsTool: ToolDefinition;
|
|
6
|
-
export declare const clearSqlCredentialsTool: ToolDefinition;
|
|
7
4
|
export declare const getSessionBindingTool: ToolDefinition;
|
|
8
5
|
export declare const selectCloudTaurusInstanceTool: ToolDefinition;
|
|
@@ -75,6 +75,7 @@ export const setCloudRegionTool = {
|
|
|
75
75
|
deps.config.cloud.region = region;
|
|
76
76
|
deps.config.cloud.apiEndpoint = buildHuaweiCloudEndpoint("gaussdb", region, domainSuffix);
|
|
77
77
|
deps.config.cloud.iamEndpoint = buildHuaweiCloudEndpoint("iam", region, domainSuffix);
|
|
78
|
+
deps.config.cloud.kmsEndpoint = buildHuaweiCloudEndpoint("kms", region, domainSuffix);
|
|
78
79
|
deps.config.slowSqlSource.taurusApi.endpoint = buildHuaweiCloudEndpoint("gaussdb", region, domainSuffix);
|
|
79
80
|
deps.config.slowSqlSource.das.endpoint = buildHuaweiCloudEndpoint("das", region, domainSuffix);
|
|
80
81
|
deps.config.metricsSource.ces.endpoint = buildHuaweiCloudEndpoint("ces", region, domainSuffix);
|
|
@@ -84,6 +85,7 @@ export const setCloudRegionTool = {
|
|
|
84
85
|
region,
|
|
85
86
|
api_endpoint: deps.config.cloud.apiEndpoint,
|
|
86
87
|
iam_endpoint: deps.config.cloud.iamEndpoint,
|
|
88
|
+
kms_endpoint: deps.config.cloud.kmsEndpoint,
|
|
87
89
|
}, {
|
|
88
90
|
summary: `Cloud region switched to ${region}.`,
|
|
89
91
|
metadata: metadata(context.taskId),
|
|
@@ -97,47 +99,6 @@ export const setCloudRegionTool = {
|
|
|
97
99
|
}
|
|
98
100
|
},
|
|
99
101
|
};
|
|
100
|
-
export const setCloudAccessKeysTool = {
|
|
101
|
-
name: "set_cloud_access_keys",
|
|
102
|
-
description: "Update the active Huawei Cloud AK/SK for the current MCP session and clear stale token or instance bindings.",
|
|
103
|
-
inputSchema: {
|
|
104
|
-
access_key_id: z.string().trim().min(1).describe("Huawei Cloud access key id."),
|
|
105
|
-
secret_access_key: z.string().trim().min(1).describe("Huawei Cloud secret access key."),
|
|
106
|
-
security_token: z.string().trim().min(1).optional().describe("Optional temporary security token when using temporary AK/SK."),
|
|
107
|
-
},
|
|
108
|
-
async handler(input, deps, context) {
|
|
109
|
-
try {
|
|
110
|
-
const accessKeyId = typeof input.access_key_id === "string" ? input.access_key_id.trim() : "";
|
|
111
|
-
const secretAccessKey = typeof input.secret_access_key === "string" ? input.secret_access_key.trim() : "";
|
|
112
|
-
const securityToken = typeof input.security_token === "string" ? input.security_token.trim() : undefined;
|
|
113
|
-
if (!accessKeyId || !secretAccessKey) {
|
|
114
|
-
throw new ToolInputError("access_key_id and secret_access_key are required.");
|
|
115
|
-
}
|
|
116
|
-
deps.config.cloud.accessKeyId = accessKeyId;
|
|
117
|
-
deps.config.cloud.secretAccessKey = secretAccessKey;
|
|
118
|
-
deps.config.cloud.securityToken = securityToken;
|
|
119
|
-
deps.config.cloud.authToken = undefined;
|
|
120
|
-
deps.config.slowSqlSource.taurusApi.authToken = undefined;
|
|
121
|
-
deps.config.slowSqlSource.das.authToken = undefined;
|
|
122
|
-
deps.config.metricsSource.ces.authToken = undefined;
|
|
123
|
-
clearCloudSelection(deps);
|
|
124
|
-
await reloadEngine(deps);
|
|
125
|
-
return formatSuccess({
|
|
126
|
-
access_key_id_suffix: accessKeyId.slice(-4),
|
|
127
|
-
uses_security_token: Boolean(securityToken),
|
|
128
|
-
}, {
|
|
129
|
-
summary: "Cloud access keys updated for the current session.",
|
|
130
|
-
metadata: metadata(context.taskId),
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
return formatToolError(error, {
|
|
135
|
-
action: "set_cloud_access_keys",
|
|
136
|
-
metadata: metadata(context.taskId),
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
102
|
export const setDefaultDatabaseTool = {
|
|
142
103
|
name: "set_default_database",
|
|
143
104
|
description: "Set the default database for a datasource in the current MCP session so later tool calls can omit input.database.",
|
|
@@ -201,120 +162,9 @@ export const setDefaultDatabaseTool = {
|
|
|
201
162
|
}
|
|
202
163
|
},
|
|
203
164
|
};
|
|
204
|
-
export const setSqlCredentialsTool = {
|
|
205
|
-
name: "set_sql_credentials",
|
|
206
|
-
description: "Set the SQL username and password for a datasource in the current MCP session without writing credentials to disk.",
|
|
207
|
-
inputSchema: {
|
|
208
|
-
username: z
|
|
209
|
-
.string()
|
|
210
|
-
.trim()
|
|
211
|
-
.min(1)
|
|
212
|
-
.describe("Database username to use for the selected datasource in this session."),
|
|
213
|
-
password: z
|
|
214
|
-
.string()
|
|
215
|
-
.trim()
|
|
216
|
-
.min(1)
|
|
217
|
-
.describe("Database password to use for the selected datasource in this session."),
|
|
218
|
-
datasource: z
|
|
219
|
-
.string()
|
|
220
|
-
.trim()
|
|
221
|
-
.min(1)
|
|
222
|
-
.optional()
|
|
223
|
-
.describe("Optional datasource template to bind. Defaults to the current default datasource."),
|
|
224
|
-
},
|
|
225
|
-
async handler(input, deps, context) {
|
|
226
|
-
try {
|
|
227
|
-
const username = typeof input.username === "string" ? input.username.trim() : "";
|
|
228
|
-
const password = typeof input.password === "string" ? input.password.trim() : "";
|
|
229
|
-
if (!username || !password) {
|
|
230
|
-
throw new ToolInputError("username and password are required.");
|
|
231
|
-
}
|
|
232
|
-
const datasource = await resolveBindingDatasource(deps, typeof input.datasource === "string" ? input.datasource : undefined);
|
|
233
|
-
if (!datasource) {
|
|
234
|
-
throw new ToolInputError("No datasource selected. Configure a default datasource or pass datasource explicitly.");
|
|
235
|
-
}
|
|
236
|
-
const profile = await deps.profileLoader.get(datasource);
|
|
237
|
-
if (!profile) {
|
|
238
|
-
throw new ToolInputError(`Datasource "${datasource}" was not found.`);
|
|
239
|
-
}
|
|
240
|
-
const currentTarget = deps.profileLoader.getRuntimeTarget(datasource);
|
|
241
|
-
deps.profileLoader.setRuntimeTarget(datasource, {
|
|
242
|
-
host: currentTarget?.host ?? profile.host,
|
|
243
|
-
port: currentTarget?.port ?? profile.port,
|
|
244
|
-
database: currentTarget?.database ?? profile.database,
|
|
245
|
-
instanceId: currentTarget?.instanceId,
|
|
246
|
-
nodeId: currentTarget?.nodeId,
|
|
247
|
-
user: {
|
|
248
|
-
username,
|
|
249
|
-
password: { type: "plain", value: password },
|
|
250
|
-
},
|
|
251
|
-
});
|
|
252
|
-
await reloadEngine(deps);
|
|
253
|
-
return formatSuccess({
|
|
254
|
-
datasource,
|
|
255
|
-
username,
|
|
256
|
-
}, {
|
|
257
|
-
summary: `SQL credentials for ${datasource} updated in the current session.`,
|
|
258
|
-
metadata: metadata(context.taskId),
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
catch (error) {
|
|
262
|
-
return formatToolError(error, {
|
|
263
|
-
action: "set_sql_credentials",
|
|
264
|
-
metadata: metadata(context.taskId),
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
},
|
|
268
|
-
};
|
|
269
|
-
export const clearSqlCredentialsTool = {
|
|
270
|
-
name: "clear_sql_credentials",
|
|
271
|
-
description: "Clear any session-scoped SQL credential override for a datasource and fall back to the configured profile credentials.",
|
|
272
|
-
inputSchema: {
|
|
273
|
-
datasource: z
|
|
274
|
-
.string()
|
|
275
|
-
.trim()
|
|
276
|
-
.min(1)
|
|
277
|
-
.optional()
|
|
278
|
-
.describe("Optional datasource template to clear. Defaults to the current default datasource."),
|
|
279
|
-
},
|
|
280
|
-
async handler(input, deps, context) {
|
|
281
|
-
try {
|
|
282
|
-
const datasource = await resolveBindingDatasource(deps, typeof input.datasource === "string" ? input.datasource : undefined);
|
|
283
|
-
if (!datasource) {
|
|
284
|
-
throw new ToolInputError("No datasource selected. Configure a default datasource or pass datasource explicitly.");
|
|
285
|
-
}
|
|
286
|
-
const profile = await deps.profileLoader.get(datasource);
|
|
287
|
-
if (!profile) {
|
|
288
|
-
throw new ToolInputError(`Datasource "${datasource}" was not found.`);
|
|
289
|
-
}
|
|
290
|
-
const currentTarget = deps.profileLoader.getRuntimeTarget(datasource);
|
|
291
|
-
deps.profileLoader.setRuntimeTarget(datasource, {
|
|
292
|
-
host: currentTarget?.host ?? profile.host,
|
|
293
|
-
port: currentTarget?.port ?? profile.port,
|
|
294
|
-
database: currentTarget?.database ?? profile.database,
|
|
295
|
-
instanceId: currentTarget?.instanceId,
|
|
296
|
-
nodeId: currentTarget?.nodeId,
|
|
297
|
-
user: profile.user,
|
|
298
|
-
});
|
|
299
|
-
await reloadEngine(deps);
|
|
300
|
-
return formatSuccess({
|
|
301
|
-
datasource,
|
|
302
|
-
}, {
|
|
303
|
-
summary: `Session SQL credential override cleared for ${datasource}.`,
|
|
304
|
-
metadata: metadata(context.taskId),
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
catch (error) {
|
|
308
|
-
return formatToolError(error, {
|
|
309
|
-
action: "clear_sql_credentials",
|
|
310
|
-
metadata: metadata(context.taskId),
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
},
|
|
314
|
-
};
|
|
315
165
|
export const getSessionBindingTool = {
|
|
316
166
|
name: "get_session_binding",
|
|
317
|
-
description: "Return the current session binding for a datasource, including selected instance, host, database, and
|
|
167
|
+
description: "Return the current session binding for a datasource, including selected instance, host, database, and configured database username.",
|
|
318
168
|
inputSchema: {
|
|
319
169
|
datasource: z
|
|
320
170
|
.string()
|
|
@@ -339,15 +189,13 @@ export const getSessionBindingTool = {
|
|
|
339
189
|
host: profile.host,
|
|
340
190
|
port: profile.port,
|
|
341
191
|
database: profile.database,
|
|
342
|
-
username_masked: maskUsername(profile.user
|
|
192
|
+
username_masked: maskUsername(profile.user?.username),
|
|
343
193
|
runtime_override: {
|
|
344
194
|
instance_id: target?.instanceId,
|
|
345
195
|
node_id: target?.nodeId,
|
|
346
196
|
host: target?.host,
|
|
347
197
|
port: target?.port,
|
|
348
198
|
database: target?.database,
|
|
349
|
-
has_sql_credentials_override: Boolean(target?.user),
|
|
350
|
-
username_masked: maskUsername(target?.user?.username),
|
|
351
199
|
},
|
|
352
200
|
}, {
|
|
353
201
|
summary: `Returned current session binding for ${datasource}.`,
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.4.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.4.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taurusdb-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "TaurusDB MCP server for Claude, Cursor, VS Code, and other MCP clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,17 +22,18 @@
|
|
|
22
22
|
"taurusdb-mcp": "dist/index.js"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
-
"dist"
|
|
25
|
+
"dist",
|
|
26
|
+
"scripts"
|
|
26
27
|
],
|
|
27
28
|
"scripts": {
|
|
28
|
-
"build": "tsc -p tsconfig.json",
|
|
29
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
29
30
|
"check": "tsc --noEmit -p tsconfig.json",
|
|
30
31
|
"dev": "tsx src/index.ts",
|
|
31
32
|
"start": "node dist/index.js",
|
|
32
33
|
"test": "node --test tests/*.test.mjs"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
35
|
-
"taurusdb-core": "0.
|
|
36
|
+
"taurusdb-core": "0.4.0",
|
|
36
37
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
37
38
|
"mysql2": "^3.22.1",
|
|
38
39
|
"zod": "^3.24.1"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(Mandatory = $true)][string]$Service,
|
|
3
|
+
[Parameter(Mandatory = $true)][string]$Account,
|
|
4
|
+
[switch]$WithSecurityToken
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
Add-Type -TypeDefinition @'
|
|
8
|
+
using System;
|
|
9
|
+
using System.Runtime.InteropServices;
|
|
10
|
+
public static class TaurusCredentialWriter {
|
|
11
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
12
|
+
public struct Credential {
|
|
13
|
+
public UInt32 Flags; public UInt32 Type; public string TargetName; public string Comment;
|
|
14
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
|
15
|
+
public UInt32 CredentialBlobSize; public IntPtr CredentialBlob; public UInt32 Persist;
|
|
16
|
+
public UInt32 AttributeCount; public IntPtr Attributes; public string TargetAlias; public string UserName;
|
|
17
|
+
}
|
|
18
|
+
[DllImport("advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
19
|
+
public static extern bool CredWrite(ref Credential credential, UInt32 flags);
|
|
20
|
+
}
|
|
21
|
+
'@
|
|
22
|
+
|
|
23
|
+
function Save-CredentialValue([string]$Key, [string]$Label) {
|
|
24
|
+
$secure = Read-Host "Enter $Label" -AsSecureString
|
|
25
|
+
$pointer = [Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($secure)
|
|
26
|
+
try {
|
|
27
|
+
$value = [Runtime.InteropServices.Marshal]::PtrToStringUni($pointer)
|
|
28
|
+
$bytes = [Text.Encoding]::Unicode.GetBytes($value)
|
|
29
|
+
$blob = [Runtime.InteropServices.Marshal]::AllocHGlobal($bytes.Length)
|
|
30
|
+
try {
|
|
31
|
+
[Runtime.InteropServices.Marshal]::Copy($bytes, 0, $blob, $bytes.Length)
|
|
32
|
+
$credential = New-Object TaurusCredentialWriter+Credential
|
|
33
|
+
$credential.Type = 1
|
|
34
|
+
$credential.TargetName = "$Service/$Account/$Key"
|
|
35
|
+
$credential.CredentialBlobSize = $bytes.Length
|
|
36
|
+
$credential.CredentialBlob = $blob
|
|
37
|
+
$credential.Persist = 2
|
|
38
|
+
$credential.UserName = $Account
|
|
39
|
+
if (-not [TaurusCredentialWriter]::CredWrite([ref]$credential, 0)) { throw "CredWriteW failed." }
|
|
40
|
+
} finally {
|
|
41
|
+
[Runtime.InteropServices.Marshal]::FreeHGlobal($blob)
|
|
42
|
+
[Array]::Clear($bytes, 0, $bytes.Length)
|
|
43
|
+
}
|
|
44
|
+
} finally {
|
|
45
|
+
[Runtime.InteropServices.Marshal]::ZeroFreeGlobalAllocUnicode($pointer)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Save-CredentialValue "access-key-id" "Huawei Cloud access key ID"
|
|
50
|
+
Save-CredentialValue "secret-access-key" "Huawei Cloud secret access key"
|
|
51
|
+
if ($WithSecurityToken) { Save-CredentialValue "security-token" "Huawei Cloud security token" }
|