taurusdb-mcp 0.3.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 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=<your-readonly-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=<your-readonly-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,3 @@
1
+ export declare function runCredentialsConfigure(args: string[]): number;
2
+ export declare function runCredentialsCheck(args: string[]): Promise<number>;
3
+ export declare function runCredentials(args: string[]): Promise<number>;
@@ -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,9 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { TaurusDBEngine, type Config, type RuntimeTargetProfileLoader } from "taurusdb-core";
3
- import { type CredentialLoginService } from "./security/local-credential-login.js";
4
3
  export interface ServerDeps {
5
4
  config: Config;
6
5
  profileLoader: RuntimeTargetProfileLoader;
7
6
  engine: TaurusDBEngine;
8
- credentialLogin: CredentialLoginService;
9
7
  pingResponse: string;
10
8
  }
11
9
  export declare function bootstrapDependencies(): Promise<ServerDeps>;
package/dist/server.js CHANGED
@@ -4,7 +4,6 @@ import { createSqlProfileLoader, getConfig, RuntimeOverrideProfileLoader, redact
4
4
  import { registerTools } from "./tools/registry.js";
5
5
  import { logger } from "taurusdb-core";
6
6
  import { VERSION } from "./version.js";
7
- import { LocalCredentialLoginService, } from "./security/local-credential-login.js";
8
7
  export async function bootstrapDependencies() {
9
8
  const config = getConfig();
10
9
  const profileLoader = new RuntimeOverrideProfileLoader(createSqlProfileLoader({ config }));
@@ -13,7 +12,6 @@ export async function bootstrapDependencies() {
13
12
  config,
14
13
  profileLoader,
15
14
  engine,
16
- credentialLogin: new LocalCredentialLoginService(),
17
15
  pingResponse: "pong",
18
16
  };
19
17
  }
@@ -24,10 +22,7 @@ export function createServer(deps) {
24
22
  });
25
23
  let cleanupPromise;
26
24
  const cleanup = () => {
27
- cleanupPromise ??= Promise.all([
28
- deps.credentialLogin.close(),
29
- deps.engine.close(),
30
- ]).then(() => undefined);
25
+ cleanupPromise ??= deps.engine.close();
31
26
  return cleanupPromise;
32
27
  };
33
28
  server.server.onclose = () => {
@@ -4,8 +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 { clearSqlCredentialsTool, getSessionBindingTool, selectCloudTaurusInstanceTool, setCloudRegionTool, setDefaultDatabaseTool, } from "./taurus/cloud-context.js";
8
- import { beginSqlLoginTool } from "./taurus/sql-login.js";
7
+ import { getSessionBindingTool, selectCloudTaurusInstanceTool, setCloudRegionTool, setDefaultDatabaseTool, } from "./taurus/cloud-context.js";
9
8
  import { listCloudTaurusInstancesTool } from "./taurus/cloud-instances.js";
10
9
  import { diagnosticToolDefinitions } from "./taurus/diagnostics.js";
11
10
  import { explainSqlEnhancedTool } from "./taurus/explain.js";
@@ -69,8 +68,6 @@ export const capabilityToolDefinitions = [
69
68
  listTaurusFeaturesTool,
70
69
  setCloudRegionTool,
71
70
  getSessionBindingTool,
72
- beginSqlLoginTool,
73
- clearSqlCredentialsTool,
74
71
  setDefaultDatabaseTool,
75
72
  listCloudTaurusInstancesTool,
76
73
  selectCloudTaurusInstanceTool,
@@ -1,6 +1,5 @@
1
1
  import type { ToolDefinition } from "../registry.js";
2
2
  export declare const setCloudRegionTool: ToolDefinition;
3
3
  export declare const setDefaultDatabaseTool: ToolDefinition;
4
- export declare const clearSqlCredentialsTool: ToolDefinition;
5
4
  export declare const getSessionBindingTool: ToolDefinition;
6
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),
@@ -160,47 +162,9 @@ export const setDefaultDatabaseTool = {
160
162
  }
161
163
  },
162
164
  };
163
- export const clearSqlCredentialsTool = {
164
- name: "clear_sql_credentials",
165
- description: "Clear any session-scoped SQL credential override for a datasource and fall back to the configured profile credentials.",
166
- inputSchema: {
167
- datasource: z
168
- .string()
169
- .trim()
170
- .min(1)
171
- .optional()
172
- .describe("Optional datasource template to clear. Defaults to the current default datasource."),
173
- },
174
- async handler(input, deps, context) {
175
- try {
176
- const datasource = await resolveBindingDatasource(deps, typeof input.datasource === "string" ? input.datasource : undefined);
177
- if (!datasource) {
178
- throw new ToolInputError("No datasource selected. Configure a default datasource or pass datasource explicitly.");
179
- }
180
- const profile = await deps.profileLoader.get(datasource);
181
- if (!profile) {
182
- throw new ToolInputError(`Datasource "${datasource}" was not found.`);
183
- }
184
- deps.profileLoader.clearRuntimeUser(datasource);
185
- await reloadEngine(deps);
186
- return formatSuccess({
187
- datasource,
188
- }, {
189
- summary: `Session SQL credential override cleared for ${datasource}.`,
190
- metadata: metadata(context.taskId),
191
- });
192
- }
193
- catch (error) {
194
- return formatToolError(error, {
195
- action: "clear_sql_credentials",
196
- metadata: metadata(context.taskId),
197
- });
198
- }
199
- },
200
- };
201
165
  export const getSessionBindingTool = {
202
166
  name: "get_session_binding",
203
- description: "Return the current session binding for a datasource, including selected instance, host, database, and whether SQL credentials are overridden in memory.",
167
+ description: "Return the current session binding for a datasource, including selected instance, host, database, and configured database username.",
204
168
  inputSchema: {
205
169
  datasource: z
206
170
  .string()
@@ -232,8 +196,6 @@ export const getSessionBindingTool = {
232
196
  host: target?.host,
233
197
  port: target?.port,
234
198
  database: target?.database,
235
- has_sql_credentials_override: Boolean(target?.user),
236
- username_masked: maskUsername(target?.user?.username),
237
199
  },
238
200
  }, {
239
201
  summary: `Returned current session binding for ${datasource}.`,
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.1.0";
1
+ export declare const VERSION = "0.4.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.1.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.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.3.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" }
@@ -1,33 +0,0 @@
1
- export type SqlLoginCredentials = {
2
- datasource: string;
3
- username: string;
4
- password: string;
5
- };
6
- export type SqlLoginRequest = {
7
- datasource: string;
8
- bind: (credentials: SqlLoginCredentials) => Promise<void>;
9
- };
10
- export type IssuedSqlLogin = {
11
- loginUrl: string;
12
- expiresAt: string;
13
- };
14
- export interface CredentialLoginService {
15
- issueSqlLogin(request: SqlLoginRequest): Promise<IssuedSqlLogin>;
16
- close(): Promise<void>;
17
- }
18
- export type LocalCredentialLoginServiceOptions = {
19
- now?: () => number;
20
- tokenTtlMs?: number;
21
- };
22
- export declare class LocalCredentialLoginService implements CredentialLoginService {
23
- private readonly now;
24
- private readonly tokenTtlMs;
25
- private readonly pending;
26
- private server;
27
- private port;
28
- constructor(options?: LocalCredentialLoginServiceOptions);
29
- issueSqlLogin(request: SqlLoginRequest): Promise<IssuedSqlLogin>;
30
- close(): Promise<void>;
31
- private ensureStarted;
32
- private handle;
33
- }
@@ -1,156 +0,0 @@
1
- import { randomBytes } from "node:crypto";
2
- import { createServer, } from "node:http";
3
- const DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1000;
4
- const MAX_REQUEST_BODY_BYTES = 16 * 1024;
5
- function page(title, message, form = false) {
6
- const formHtml = form
7
- ? `<form method="post">
8
- <label>Database username <input name="username" autocomplete="username" required></label>
9
- <label>Database password <input name="password" type="password" autocomplete="current-password" required></label>
10
- <button type="submit">Connect</button>
11
- </form>`
12
- : "";
13
- return `<!doctype html>
14
- <html lang="en">
15
- <head>
16
- <meta charset="utf-8">
17
- <meta name="viewport" content="width=device-width, initial-scale=1">
18
- <title>${title}</title>
19
- <style>
20
- body { font-family: system-ui, sans-serif; max-width: 32rem; margin: 4rem auto; padding: 0 1rem; }
21
- form { display: grid; gap: 1rem; }
22
- label { display: grid; gap: .35rem; }
23
- input, button { font: inherit; padding: .65rem; }
24
- </style>
25
- </head>
26
- <body>
27
- <h1>${title}</h1>
28
- <p>${message}</p>
29
- ${formHtml}
30
- </body>
31
- </html>`;
32
- }
33
- function respond(res, status, body) {
34
- res.writeHead(status, {
35
- "cache-control": "no-store",
36
- "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'",
37
- "content-type": "text/html; charset=utf-8",
38
- "referrer-policy": "no-referrer",
39
- "x-content-type-options": "nosniff",
40
- });
41
- res.end(body);
42
- }
43
- async function readBody(req) {
44
- const chunks = [];
45
- let size = 0;
46
- for await (const chunk of req) {
47
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
48
- size += buffer.length;
49
- if (size > MAX_REQUEST_BODY_BYTES) {
50
- throw new Error("Request body is too large.");
51
- }
52
- chunks.push(buffer);
53
- }
54
- return Buffer.concat(chunks).toString("utf8");
55
- }
56
- export class LocalCredentialLoginService {
57
- now;
58
- tokenTtlMs;
59
- pending = new Map();
60
- server;
61
- port;
62
- constructor(options = {}) {
63
- this.now = options.now ?? Date.now;
64
- this.tokenTtlMs = options.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;
65
- }
66
- async issueSqlLogin(request) {
67
- await this.ensureStarted();
68
- const token = randomBytes(32).toString("base64url");
69
- const expiresAtMs = this.now() + this.tokenTtlMs;
70
- this.pending.set(token, { ...request, expiresAtMs });
71
- return {
72
- loginUrl: `http://127.0.0.1:${this.port}/sql-login/${token}`,
73
- expiresAt: new Date(expiresAtMs).toISOString(),
74
- };
75
- }
76
- async close() {
77
- this.pending.clear();
78
- const server = this.server;
79
- this.server = undefined;
80
- this.port = undefined;
81
- if (!server) {
82
- return;
83
- }
84
- await new Promise((resolve, reject) => {
85
- server.close((error) => (error ? reject(error) : resolve()));
86
- });
87
- }
88
- async ensureStarted() {
89
- if (this.server) {
90
- return;
91
- }
92
- const server = createServer((req, res) => {
93
- void this.handle(req, res);
94
- });
95
- await new Promise((resolve, reject) => {
96
- server.once("error", reject);
97
- server.listen(0, "127.0.0.1", () => {
98
- server.off("error", reject);
99
- resolve();
100
- });
101
- });
102
- const address = server.address();
103
- if (!address || typeof address === "string") {
104
- await new Promise((resolve) => server.close(() => resolve()));
105
- throw new Error("Unable to resolve local credential login address.");
106
- }
107
- this.server = server;
108
- this.port = address.port;
109
- }
110
- async handle(req, res) {
111
- try {
112
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
113
- const match = url.pathname.match(/^\/sql-login\/([^/]+)$/);
114
- const token = match?.[1];
115
- const pending = token ? this.pending.get(token) : undefined;
116
- if (!token || !pending || pending.expiresAtMs <= this.now()) {
117
- if (token) {
118
- this.pending.delete(token);
119
- }
120
- respond(res, 410, page("Invalid login link", "This login link is invalid or has expired."));
121
- return;
122
- }
123
- if (req.method === "GET") {
124
- respond(res, 200, page("Connect to TaurusDB", "Enter your database credentials.", true));
125
- return;
126
- }
127
- if (req.method !== "POST") {
128
- respond(res, 405, page("Unsupported request", "Use the login form to continue."));
129
- return;
130
- }
131
- const form = new URLSearchParams(await readBody(req));
132
- const username = form.get("username")?.trim() ?? "";
133
- const password = form.get("password") ?? "";
134
- if (!username || !password) {
135
- respond(res, 400, page("Connect to TaurusDB", "Username and password are required.", true));
136
- return;
137
- }
138
- this.pending.delete(token);
139
- try {
140
- await pending.bind({
141
- datasource: pending.datasource,
142
- username,
143
- password,
144
- });
145
- }
146
- catch {
147
- respond(res, 500, page("Connection failed", "Credentials could not be bound to this session."));
148
- return;
149
- }
150
- respond(res, 200, page("Connected", "Database credentials are now active for this MCP session."));
151
- }
152
- catch {
153
- respond(res, 400, page("Invalid request", "The login request could not be processed."));
154
- }
155
- }
156
- }
@@ -1,2 +0,0 @@
1
- import type { ToolDefinition } from "../registry.js";
2
- export declare const beginSqlLoginTool: ToolDefinition;
@@ -1,70 +0,0 @@
1
- import { TaurusDBEngine } from "taurusdb-core";
2
- import { z } from "zod";
3
- import { formatSuccess } from "../../utils/formatter.js";
4
- import { metadata } from "../common.js";
5
- import { formatToolError, ToolInputError } from "../error-handling.js";
6
- export const beginSqlLoginTool = {
7
- name: "begin_sql_login",
8
- description: "Create a short-lived local login URL where the user can enter SQL credentials without sending the password through the Agent or MCP tool arguments.",
9
- inputSchema: {
10
- datasource: z
11
- .string()
12
- .trim()
13
- .min(1)
14
- .optional()
15
- .describe("Optional datasource template. Defaults to the current default datasource."),
16
- },
17
- async handler(input, deps, context) {
18
- try {
19
- const explicit = typeof input.datasource === "string" ? input.datasource.trim() : "";
20
- const datasource = explicit || (await deps.engine.getDefaultDataSource());
21
- if (!datasource) {
22
- throw new ToolInputError("No datasource selected. Configure a default datasource or pass datasource explicitly.");
23
- }
24
- const profile = await deps.profileLoader.get(datasource);
25
- if (!profile) {
26
- throw new ToolInputError(`Datasource "${datasource}" was not found.`);
27
- }
28
- const issued = await deps.credentialLogin.issueSqlLogin({
29
- datasource,
30
- bind: async ({ username, password }) => {
31
- const currentTarget = deps.profileLoader.getRuntimeTarget(datasource);
32
- deps.profileLoader.setRuntimeTarget(datasource, {
33
- host: currentTarget?.host ?? profile.host,
34
- port: currentTarget?.port ?? profile.port,
35
- database: currentTarget?.database ?? profile.database,
36
- instanceId: currentTarget?.instanceId,
37
- nodeId: currentTarget?.nodeId,
38
- user: {
39
- username,
40
- password: { type: "plain", value: password },
41
- },
42
- });
43
- const nextEngine = await TaurusDBEngine.create({
44
- config: deps.config,
45
- profileLoader: deps.profileLoader,
46
- });
47
- const previousEngine = deps.engine;
48
- deps.engine = nextEngine;
49
- if (previousEngine?.close) {
50
- await previousEngine.close();
51
- }
52
- },
53
- });
54
- return formatSuccess({
55
- datasource,
56
- login_url: issued.loginUrl,
57
- expires_at: issued.expiresAt,
58
- }, {
59
- summary: `Created a secure local SQL login link for ${datasource}.`,
60
- metadata: metadata(context.taskId),
61
- });
62
- }
63
- catch (error) {
64
- return formatToolError(error, {
65
- action: "begin_sql_login",
66
- metadata: metadata(context.taskId),
67
- });
68
- }
69
- },
70
- };