taurusdb-mcp 0.4.0 → 0.5.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +40 -4
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +66 -0
- package/dist/index.js +5 -0
- package/dist/security/local-credential-login.d.ts +33 -0
- package/dist/security/local-credential-login.js +156 -0
- package/dist/security/session-coordinator.d.ts +9 -0
- package/dist/security/session-coordinator.js +46 -0
- package/dist/server.d.ts +10 -1
- package/dist/server.js +17 -2
- package/dist/tools/common/public-formatters.d.ts +8 -0
- package/dist/tools/common/public-formatters.js +4 -0
- package/dist/tools/error-handling.js +10 -1
- package/dist/tools/query.js +18 -13
- package/dist/tools/registry.d.ts +5 -0
- package/dist/tools/registry.js +131 -8
- package/dist/tools/taurus/cloud-context.d.ts +1 -0
- package/dist/tools/taurus/cloud-context.js +123 -53
- package/dist/tools/taurus/diagnostics.d.ts +1 -0
- package/dist/tools/taurus/diagnostics.js +31 -0
- package/dist/tools/taurus/recycle-bin.js +15 -11
- package/dist/tools/taurus/sql-login.d.ts +2 -0
- package/dist/tools/taurus/sql-login.js +90 -0
- package/package.json +14 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TaurusDB MCP contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -28,14 +28,24 @@ Use this command in MCP client configs:
|
|
|
28
28
|
}
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
Configure Huawei Cloud identity through the operating-system credential store
|
|
32
|
+
before adding the server. AK/SK values should not be copied into MCP client
|
|
33
|
+
configuration:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx -y taurusdb-mcp credentials configure
|
|
37
|
+
TAURUSDB_CLOUD_KEYCHAIN_SERVICE=taurusdb-mcp/huaweicloud \
|
|
38
|
+
npx -y taurusdb-mcp credentials check
|
|
39
|
+
```
|
|
40
|
+
|
|
31
41
|
Claude Code:
|
|
32
42
|
|
|
33
43
|
```bash
|
|
34
44
|
claude mcp add huaweicloud-taurusdb \
|
|
35
45
|
--transport stdio \
|
|
36
46
|
-e TAURUSDB_CLOUD_REGION=<your-region> \
|
|
37
|
-
-e
|
|
38
|
-
-e
|
|
47
|
+
-e TAURUSDB_CLOUD_KEYCHAIN_SERVICE=taurusdb-mcp/huaweicloud \
|
|
48
|
+
-e TAURUSDB_ENABLE_DYNAMIC_TARGETS=true \
|
|
39
49
|
-e TAURUSDB_SQL_DATABASE=<your-database> \
|
|
40
50
|
-e TAURUSDB_SQL_USER=<your-readonly-user> \
|
|
41
51
|
-e TAURUSDB_SQL_PASSWORD='hw-kms-file:~/.taurusdb-mcp/password.ciphertext' \
|
|
@@ -47,8 +57,8 @@ Codex:
|
|
|
47
57
|
```bash
|
|
48
58
|
codex mcp add huaweicloud-taurusdb \
|
|
49
59
|
--env TAURUSDB_CLOUD_REGION=<your-region> \
|
|
50
|
-
--env
|
|
51
|
-
--env
|
|
60
|
+
--env TAURUSDB_CLOUD_KEYCHAIN_SERVICE=taurusdb-mcp/huaweicloud \
|
|
61
|
+
--env TAURUSDB_ENABLE_DYNAMIC_TARGETS=true \
|
|
52
62
|
--env TAURUSDB_SQL_DATABASE=<your-database> \
|
|
53
63
|
--env TAURUSDB_SQL_USER=<your-readonly-user> \
|
|
54
64
|
--env TAURUSDB_SQL_PASSWORD='hw-kms-file:~/.taurusdb-mcp/password.ciphertext' \
|
|
@@ -77,3 +87,29 @@ npx taurusdb-mcp credentials check
|
|
|
77
87
|
- Database passwords support `env:`, `file:`, `hw-csms:`, `hw-kms:`, and `hw-kms-file:` references
|
|
78
88
|
- Huawei DEW CSMS is recommended when the database password should be stored and retrieved from Huawei Cloud
|
|
79
89
|
- macOS Keychain, Linux Secret Service, or Windows Credential Manager cloud identity is enabled with `TAURUSDB_CLOUD_KEYCHAIN_SERVICE`
|
|
90
|
+
- SQL TLS with certificate verification is required by default
|
|
91
|
+
- Mutation and dynamic-target tools are disabled by default
|
|
92
|
+
- Mutation tools require a dedicated `TAURUSDB_SQL_MUTATION_USER` /
|
|
93
|
+
`TAURUSDB_SQL_MUTATION_PASSWORD` pair and an external operator-signed approval
|
|
94
|
+
- MCP audit events are written to `~/.taurusdb-mcp/audit.jsonl` by default, with
|
|
95
|
+
configurable size rotation for collection into centralized immutable storage
|
|
96
|
+
- Run one stdio process per customer/client trust boundary; this package is not
|
|
97
|
+
a shared multi-tenant HTTP service
|
|
98
|
+
|
|
99
|
+
Enable mutations only when required:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
export TAURUSDB_ENABLE_MUTATIONS=true
|
|
103
|
+
export TAURUSDB_SQL_MUTATION_USER='<dedicated-writer>'
|
|
104
|
+
export TAURUSDB_SQL_MUTATION_PASSWORD='hw-csms:<writer-secret-name>'
|
|
105
|
+
export TAURUSDB_MUTATION_APPROVAL_SECRET_FILE='/run/secrets/taurusdb-approval'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Sign a returned `approval_request` outside the MCP client:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npx taurusdb-mcp approve \
|
|
112
|
+
--request '<approval_request>' \
|
|
113
|
+
--actor '<operator-identity>' \
|
|
114
|
+
--secret-file /run/secrets/taurusdb-approval
|
|
115
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runApprove(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { signApprovalRequest } from "taurusdb-core";
|
|
5
|
+
function readOption(args, name) {
|
|
6
|
+
const index = args.indexOf(name);
|
|
7
|
+
if (index === -1) {
|
|
8
|
+
return undefined;
|
|
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 expandTilde(input) {
|
|
17
|
+
if (input === "~") {
|
|
18
|
+
return os.homedir();
|
|
19
|
+
}
|
|
20
|
+
return input.startsWith("~/") ? path.join(os.homedir(), input.slice(2)) : input;
|
|
21
|
+
}
|
|
22
|
+
function usage() {
|
|
23
|
+
return `Usage: taurusdb-mcp approve --request <approval-request> --actor <identity> [options]
|
|
24
|
+
|
|
25
|
+
Options:
|
|
26
|
+
--secret-file <path> Approval secret file; defaults to
|
|
27
|
+
TAURUSDB_MUTATION_APPROVAL_SECRET_FILE
|
|
28
|
+
--request <value> approval_request returned by a mutation tool
|
|
29
|
+
--actor <identity> Human approver identity recorded in the signed token
|
|
30
|
+
|
|
31
|
+
The command prints a one-time approval_token. Run it outside the MCP client
|
|
32
|
+
after reviewing the SQL hash, datasource, database, risk, and expiry encoded
|
|
33
|
+
in approval_request.`;
|
|
34
|
+
}
|
|
35
|
+
async function readProtectedSecret(filePath) {
|
|
36
|
+
const resolved = expandTilde(filePath);
|
|
37
|
+
const info = await stat(resolved);
|
|
38
|
+
if (!info.isFile()) {
|
|
39
|
+
throw new Error("Mutation approval secret path must reference a regular file.");
|
|
40
|
+
}
|
|
41
|
+
if (process.platform !== "win32" && (info.mode & 0o077) !== 0) {
|
|
42
|
+
throw new Error("Mutation approval secret file must not be accessible by group or other users.");
|
|
43
|
+
}
|
|
44
|
+
const secret = (await readFile(resolved, "utf8")).trim();
|
|
45
|
+
if (Buffer.byteLength(secret, "utf8") < 32) {
|
|
46
|
+
throw new Error("Mutation approval secret must contain at least 32 bytes.");
|
|
47
|
+
}
|
|
48
|
+
return secret;
|
|
49
|
+
}
|
|
50
|
+
export async function runApprove(args) {
|
|
51
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
52
|
+
process.stdout.write(`${usage()}\n`);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
const request = readOption(args, "--request");
|
|
56
|
+
const actor = readOption(args, "--actor");
|
|
57
|
+
const secretFile = readOption(args, "--secret-file") ??
|
|
58
|
+
process.env.TAURUSDB_MUTATION_APPROVAL_SECRET_FILE?.trim();
|
|
59
|
+
if (!request || !actor || !secretFile) {
|
|
60
|
+
process.stderr.write(`${usage()}\n`);
|
|
61
|
+
return 2;
|
|
62
|
+
}
|
|
63
|
+
const secret = await readProtectedSecret(secretFile);
|
|
64
|
+
process.stdout.write(`${signApprovalRequest(request, actor, secret)}\n`);
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runInit } from "./commands/init.js";
|
|
3
3
|
import { runCredentials } from "./commands/credentials.js";
|
|
4
|
+
import { runApprove } from "./commands/approve.js";
|
|
4
5
|
import { startMcpServer } from "./server.js";
|
|
5
6
|
import { VERSION } from "./version.js";
|
|
6
7
|
async function main() {
|
|
@@ -14,6 +15,10 @@ async function main() {
|
|
|
14
15
|
process.exitCode = await runCredentials(args.slice(1));
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
18
|
+
if (firstArg === "approve") {
|
|
19
|
+
process.exitCode = await runApprove(args.slice(1));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
17
22
|
if (firstArg === "--version" || firstArg === "-v") {
|
|
18
23
|
process.stdout.write(`${VERSION}\n`);
|
|
19
24
|
return;
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class SessionCoordinator {
|
|
2
|
+
private readers;
|
|
3
|
+
private writerActive;
|
|
4
|
+
private readonly readerQueue;
|
|
5
|
+
private readonly writerQueue;
|
|
6
|
+
runShared<T>(operation: () => Promise<T>): Promise<T>;
|
|
7
|
+
runExclusive<T>(operation: () => Promise<T>): Promise<T>;
|
|
8
|
+
private drain;
|
|
9
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export class SessionCoordinator {
|
|
2
|
+
readers = 0;
|
|
3
|
+
writerActive = false;
|
|
4
|
+
readerQueue = [];
|
|
5
|
+
writerQueue = [];
|
|
6
|
+
async runShared(operation) {
|
|
7
|
+
if (this.writerActive || this.writerQueue.length > 0) {
|
|
8
|
+
await new Promise((resolve) => this.readerQueue.push(resolve));
|
|
9
|
+
}
|
|
10
|
+
this.readers += 1;
|
|
11
|
+
try {
|
|
12
|
+
return await operation();
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
this.readers -= 1;
|
|
16
|
+
this.drain();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
runExclusive(operation) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
this.writerQueue.push({
|
|
22
|
+
operation,
|
|
23
|
+
resolve: resolve,
|
|
24
|
+
reject,
|
|
25
|
+
});
|
|
26
|
+
this.drain();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
drain() {
|
|
30
|
+
if (this.writerActive || this.readers > 0) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const writer = this.writerQueue.shift();
|
|
34
|
+
if (writer) {
|
|
35
|
+
this.writerActive = true;
|
|
36
|
+
void writer.operation().then(writer.resolve, writer.reject).finally(() => {
|
|
37
|
+
this.writerActive = false;
|
|
38
|
+
this.drain();
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
for (const resolve of this.readerQueue.splice(0)) {
|
|
43
|
+
resolve();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { TaurusDBEngine, type Config, type RuntimeTargetProfileLoader } from "taurusdb-core";
|
|
2
|
+
import { TaurusDBEngine, type Config, type AuditWriter, type RuntimeTargetProfileLoader } from "taurusdb-core";
|
|
3
|
+
import { type CredentialLoginService } from "./security/local-credential-login.js";
|
|
4
|
+
import { SessionCoordinator } from "./security/session-coordinator.js";
|
|
3
5
|
export interface ServerDeps {
|
|
4
6
|
config: Config;
|
|
5
7
|
profileLoader: RuntimeTargetProfileLoader;
|
|
6
8
|
engine: TaurusDBEngine;
|
|
9
|
+
credentialLogin: CredentialLoginService;
|
|
10
|
+
auditWriter?: AuditWriter;
|
|
11
|
+
sessionCoordinator?: SessionCoordinator;
|
|
12
|
+
clientIdentityProvider?: () => {
|
|
13
|
+
name: string;
|
|
14
|
+
version: string;
|
|
15
|
+
} | undefined;
|
|
7
16
|
pingResponse: string;
|
|
8
17
|
}
|
|
9
18
|
export declare function bootstrapDependencies(): Promise<ServerDeps>;
|
package/dist/server.js
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import { createSqlProfileLoader, getConfig, RuntimeOverrideProfileLoader, redactConfigForLog, TaurusDBEngine, } from "taurusdb-core";
|
|
3
|
+
import { createSqlProfileLoader, createJsonlAuditWriter, getConfig, RuntimeOverrideProfileLoader, redactConfigForLog, TaurusDBEngine, } from "taurusdb-core";
|
|
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
|
+
import { SessionCoordinator } from "./security/session-coordinator.js";
|
|
7
9
|
export async function bootstrapDependencies() {
|
|
8
10
|
const config = getConfig();
|
|
9
11
|
const profileLoader = new RuntimeOverrideProfileLoader(createSqlProfileLoader({ config }));
|
|
10
12
|
const engine = await TaurusDBEngine.create({ config, profileLoader });
|
|
13
|
+
const auditWriter = await createJsonlAuditWriter({
|
|
14
|
+
logPath: config.audit.logPath,
|
|
15
|
+
maxBytes: config.audit.maxBytes,
|
|
16
|
+
maxFiles: config.audit.maxFiles,
|
|
17
|
+
});
|
|
11
18
|
return {
|
|
12
19
|
config,
|
|
13
20
|
profileLoader,
|
|
14
21
|
engine,
|
|
22
|
+
credentialLogin: new LocalCredentialLoginService(),
|
|
23
|
+
auditWriter,
|
|
24
|
+
sessionCoordinator: new SessionCoordinator(),
|
|
15
25
|
pingResponse: "pong",
|
|
16
26
|
};
|
|
17
27
|
}
|
|
@@ -20,9 +30,14 @@ export function createServer(deps) {
|
|
|
20
30
|
name: "huaweicloud-taurusdb",
|
|
21
31
|
version: VERSION,
|
|
22
32
|
});
|
|
33
|
+
deps.clientIdentityProvider = () => server.server.getClientVersion();
|
|
23
34
|
let cleanupPromise;
|
|
24
35
|
const cleanup = () => {
|
|
25
|
-
cleanupPromise ??=
|
|
36
|
+
cleanupPromise ??= Promise.all([
|
|
37
|
+
deps.credentialLogin.close(),
|
|
38
|
+
deps.engine.close(),
|
|
39
|
+
deps.auditWriter?.close() ?? Promise.resolve(),
|
|
40
|
+
]).then(() => undefined);
|
|
26
41
|
return cleanupPromise;
|
|
27
42
|
};
|
|
28
43
|
server.server.onclose = () => {
|
|
@@ -57,6 +57,8 @@ export declare function toPublicQueryResult(result: QueryResult): {
|
|
|
57
57
|
row_truncated: boolean;
|
|
58
58
|
column_truncated: boolean;
|
|
59
59
|
field_truncated: boolean;
|
|
60
|
+
byte_truncated: boolean;
|
|
61
|
+
returned_bytes: number;
|
|
60
62
|
redacted_columns: string[];
|
|
61
63
|
dropped_columns: string[];
|
|
62
64
|
truncated_columns: string[];
|
|
@@ -78,6 +80,8 @@ export declare function toPublicGuardrailDecision(decision: GuardrailDecision):
|
|
|
78
80
|
max_rows: number;
|
|
79
81
|
max_columns: number;
|
|
80
82
|
max_field_chars: number;
|
|
83
|
+
max_result_bytes: number;
|
|
84
|
+
max_blob_bytes: number;
|
|
81
85
|
};
|
|
82
86
|
};
|
|
83
87
|
export declare function toPublicExplainResult(result: ExplainResult, decision: GuardrailDecision): {
|
|
@@ -105,6 +109,8 @@ export declare function toPublicExplainResult(result: ExplainResult, decision: G
|
|
|
105
109
|
max_rows: number;
|
|
106
110
|
max_columns: number;
|
|
107
111
|
max_field_chars: number;
|
|
112
|
+
max_result_bytes: number;
|
|
113
|
+
max_blob_bytes: number;
|
|
108
114
|
};
|
|
109
115
|
};
|
|
110
116
|
};
|
|
@@ -146,6 +152,8 @@ export declare function toPublicEnhancedExplainResult(result: EnhancedExplainRes
|
|
|
146
152
|
max_rows: number;
|
|
147
153
|
max_columns: number;
|
|
148
154
|
max_field_chars: number;
|
|
155
|
+
max_result_bytes: number;
|
|
156
|
+
max_blob_bytes: number;
|
|
149
157
|
};
|
|
150
158
|
};
|
|
151
159
|
};
|
|
@@ -67,6 +67,8 @@ export function toPublicQueryResult(result) {
|
|
|
67
67
|
row_truncated: result.rowTruncated,
|
|
68
68
|
column_truncated: result.columnTruncated,
|
|
69
69
|
field_truncated: result.fieldTruncated,
|
|
70
|
+
byte_truncated: result.byteTruncated,
|
|
71
|
+
returned_bytes: result.returnedBytes,
|
|
70
72
|
redacted_columns: result.redactedColumns,
|
|
71
73
|
dropped_columns: result.droppedColumns,
|
|
72
74
|
truncated_columns: result.truncatedColumns,
|
|
@@ -92,6 +94,8 @@ export function toPublicGuardrailDecision(decision) {
|
|
|
92
94
|
max_rows: decision.runtimeLimits.maxRows,
|
|
93
95
|
max_columns: decision.runtimeLimits.maxColumns,
|
|
94
96
|
max_field_chars: decision.runtimeLimits.maxFieldChars,
|
|
97
|
+
max_result_bytes: decision.runtimeLimits.maxResultBytes,
|
|
98
|
+
max_blob_bytes: decision.runtimeLimits.maxBlobBytes,
|
|
95
99
|
},
|
|
96
100
|
};
|
|
97
101
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConnectionPoolError, DatasourceResolutionError, FlashbackNoViewError, logger, SchemaIntrospectionError, UnsupportedFeatureError, } from "taurusdb-core";
|
|
1
|
+
import { ConnectionPoolError, DatasourceResolutionError, FlashbackNoViewError, logger, QueryConcurrencyError, 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) {
|
|
@@ -100,6 +100,15 @@ export function formatToolError(error, context) {
|
|
|
100
100
|
details: detailsOf(error),
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
|
+
if (error instanceof QueryConcurrencyError) {
|
|
104
|
+
return formatError({
|
|
105
|
+
code: ErrorCode.SERVER_BUSY,
|
|
106
|
+
message: error.message,
|
|
107
|
+
summary: `${context.action} could not start because query capacity is exhausted.`,
|
|
108
|
+
metadata: context.metadata,
|
|
109
|
+
retryable: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
103
112
|
if (cancelledLikely(error)) {
|
|
104
113
|
return formatError({
|
|
105
114
|
code: ErrorCode.QUERY_CANCELLED,
|
package/dist/tools/query.js
CHANGED
|
@@ -12,23 +12,24 @@ async function inspectSql(toolName, sql, ctx, deps) {
|
|
|
12
12
|
context: ctx,
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
-
async function ensureConfirmation(decision, ctx, deps,
|
|
15
|
+
async function ensureConfirmation(decision, ctx, deps, invokeContext, approvalToken) {
|
|
16
16
|
if (!decision.requiresConfirmation) {
|
|
17
17
|
return undefined;
|
|
18
18
|
}
|
|
19
|
-
const responseMetadata = metadata(taskId, {
|
|
19
|
+
const responseMetadata = metadata(invokeContext.taskId, {
|
|
20
20
|
sql_hash: decision.sqlHash,
|
|
21
21
|
statement_type: statementTypeFromSql(decision.normalizedSql),
|
|
22
22
|
});
|
|
23
|
-
if (
|
|
24
|
-
const validation = await deps.engine.validateConfirmation(
|
|
23
|
+
if (approvalToken) {
|
|
24
|
+
const validation = await deps.engine.validateConfirmation(approvalToken, decision.normalizedSql, ctx);
|
|
25
25
|
if (validation.valid) {
|
|
26
|
+
invokeContext.approvalActor = validation.actor;
|
|
26
27
|
return undefined;
|
|
27
28
|
}
|
|
28
29
|
return formatError({
|
|
29
30
|
code: ErrorCode.CONFIRMATION_INVALID,
|
|
30
|
-
message: validation.reason ?? "
|
|
31
|
-
summary: "The provided
|
|
31
|
+
message: validation.reason ?? "Approval token validation failed.",
|
|
32
|
+
summary: "The provided external approval is invalid for this SQL statement.",
|
|
32
33
|
metadata: responseMetadata,
|
|
33
34
|
details: {
|
|
34
35
|
reason_codes: validation.reasonCodes,
|
|
@@ -37,9 +38,10 @@ async function ensureConfirmation(decision, ctx, deps, taskId, confirmationToken
|
|
|
37
38
|
});
|
|
38
39
|
}
|
|
39
40
|
const outcome = await deps.engine.handleConfirmation(decision, ctx);
|
|
40
|
-
if (outcome.status === "
|
|
41
|
+
if (outcome.status === "approval_required") {
|
|
41
42
|
return formatConfirmationRequired({
|
|
42
|
-
|
|
43
|
+
approvalRequest: outcome.request,
|
|
44
|
+
requestId: outcome.requestId,
|
|
43
45
|
metadata: responseMetadata,
|
|
44
46
|
riskLevel: decision.riskLevel,
|
|
45
47
|
sqlHash: decision.sqlHash,
|
|
@@ -53,7 +55,7 @@ export const executeReadonlySqlTool = {
|
|
|
53
55
|
inputSchema: {
|
|
54
56
|
...contextInputShape,
|
|
55
57
|
sql: requiredSqlSchema("Readonly SQL to execute."),
|
|
56
|
-
|
|
58
|
+
approval_token: optionalTokenSchema(),
|
|
57
59
|
},
|
|
58
60
|
async handler(input, deps, context) {
|
|
59
61
|
const sql = asRequiredString(input.sql, "sql");
|
|
@@ -76,7 +78,7 @@ export const executeReadonlySqlTool = {
|
|
|
76
78
|
},
|
|
77
79
|
});
|
|
78
80
|
}
|
|
79
|
-
const confirmationResponse = await ensureConfirmation(decision, ctx, deps, context
|
|
81
|
+
const confirmationResponse = await ensureConfirmation(decision, ctx, deps, context, asOptionalString(input.approval_token, "approval_token"));
|
|
80
82
|
if (confirmationResponse) {
|
|
81
83
|
return confirmationResponse;
|
|
82
84
|
}
|
|
@@ -85,6 +87,9 @@ export const executeReadonlySqlTool = {
|
|
|
85
87
|
maxRows: decision.runtimeLimits.maxRows,
|
|
86
88
|
maxColumns: decision.runtimeLimits.maxColumns,
|
|
87
89
|
maxFieldChars: decision.runtimeLimits.maxFieldChars,
|
|
90
|
+
maxResultBytes: decision.runtimeLimits.maxResultBytes,
|
|
91
|
+
maxBlobBytes: decision.runtimeLimits.maxBlobBytes,
|
|
92
|
+
maskAllColumns: decision.runtimeLimits.maskAllColumns,
|
|
88
93
|
});
|
|
89
94
|
return formatSuccess(toPublicQueryResult(result), {
|
|
90
95
|
summary: summarizeRows(result.rowCount, result.truncated),
|
|
@@ -161,7 +166,7 @@ export const executeSqlTool = {
|
|
|
161
166
|
inputSchema: {
|
|
162
167
|
...contextInputShape,
|
|
163
168
|
sql: requiredSqlSchema("Mutation SQL to execute."),
|
|
164
|
-
|
|
169
|
+
approval_token: optionalTokenSchema(),
|
|
165
170
|
},
|
|
166
171
|
async handler(input, deps, context) {
|
|
167
172
|
const sql = asRequiredString(input.sql, "sql");
|
|
@@ -184,7 +189,7 @@ export const executeSqlTool = {
|
|
|
184
189
|
},
|
|
185
190
|
});
|
|
186
191
|
}
|
|
187
|
-
const confirmationResponse = await ensureConfirmation(decision, ctx, deps, context
|
|
192
|
+
const confirmationResponse = await ensureConfirmation(decision, ctx, deps, context, asOptionalString(input.approval_token, "approval_token"));
|
|
188
193
|
if (confirmationResponse) {
|
|
189
194
|
return confirmationResponse;
|
|
190
195
|
}
|
|
@@ -219,5 +224,5 @@ function optionalTokenSchema() {
|
|
|
219
224
|
.trim()
|
|
220
225
|
.min(1)
|
|
221
226
|
.optional()
|
|
222
|
-
.describe("
|
|
227
|
+
.describe("Externally signed approval token produced by `taurusdb-mcp approve`.");
|
|
223
228
|
}
|