taurusdb-mcp 0.1.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 +32 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +195 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +22 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +60 -0
- package/dist/tools/common/input.d.ts +50 -0
- package/dist/tools/common/input.js +168 -0
- package/dist/tools/common/public-formatters.d.ts +344 -0
- package/dist/tools/common/public-formatters.js +348 -0
- package/dist/tools/common.d.ts +2 -0
- package/dist/tools/common.js +2 -0
- package/dist/tools/discovery.d.ts +5 -0
- package/dist/tools/discovery.js +113 -0
- package/dist/tools/error-handling.d.ts +10 -0
- package/dist/tools/error-handling.js +122 -0
- package/dist/tools/ping.d.ts +4 -0
- package/dist/tools/ping.js +13 -0
- package/dist/tools/processlist.d.ts +2 -0
- package/dist/tools/processlist.js +121 -0
- package/dist/tools/query.d.ts +4 -0
- package/dist/tools/query.js +224 -0
- package/dist/tools/registry.d.ts +22 -0
- package/dist/tools/registry.js +106 -0
- package/dist/tools/taurus/capability.d.ts +3 -0
- package/dist/tools/taurus/capability.js +66 -0
- package/dist/tools/taurus/cloud-context.d.ts +4 -0
- package/dist/tools/taurus/cloud-context.js +209 -0
- package/dist/tools/taurus/cloud-instances.d.ts +2 -0
- package/dist/tools/taurus/cloud-instances.js +73 -0
- package/dist/tools/taurus/diagnostics.d.ts +9 -0
- package/dist/tools/taurus/diagnostics.js +323 -0
- package/dist/tools/taurus/explain.d.ts +2 -0
- package/dist/tools/taurus/explain.js +69 -0
- package/dist/tools/taurus/flashback.d.ts +2 -0
- package/dist/tools/taurus/flashback.js +101 -0
- package/dist/tools/taurus/recycle-bin.d.ts +3 -0
- package/dist/tools/taurus/recycle-bin.js +148 -0
- package/dist/utils/formatter.d.ts +5 -0
- package/dist/utils/formatter.js +8 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatSuccess } from "../utils/formatter.js";
|
|
3
|
+
import { formatToolError } from "./error-handling.js";
|
|
4
|
+
import { contextInputShape, metadata, requireDatabase, resolveContext, toPublicDataSourceInfo, toPublicDatabaseInfo, toPublicTableInfo, toPublicTableSchema, asRequiredString, } from "./common.js";
|
|
5
|
+
export const listDataSourcesTool = {
|
|
6
|
+
name: "list_data_sources",
|
|
7
|
+
description: "List all configured datasource profiles and indicate the current default datasource.",
|
|
8
|
+
inputSchema: {},
|
|
9
|
+
async handler(_input, deps, context) {
|
|
10
|
+
try {
|
|
11
|
+
const [items, defaultDatasource] = await Promise.all([
|
|
12
|
+
deps.engine.listDataSources(),
|
|
13
|
+
deps.engine.getDefaultDataSource(),
|
|
14
|
+
]);
|
|
15
|
+
return formatSuccess({
|
|
16
|
+
items: items.map(toPublicDataSourceInfo),
|
|
17
|
+
default_datasource: defaultDatasource,
|
|
18
|
+
}, {
|
|
19
|
+
summary: `Resolved ${items.length} datasource profiles.`,
|
|
20
|
+
metadata: metadata(context.taskId),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return formatToolError(error, {
|
|
25
|
+
action: "list_data_sources",
|
|
26
|
+
metadata: metadata(context.taskId),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
export const listDatabasesTool = {
|
|
32
|
+
name: "list_databases",
|
|
33
|
+
description: "List databases available on the selected datasource.",
|
|
34
|
+
inputSchema: contextInputShape,
|
|
35
|
+
async handler(input, deps, context) {
|
|
36
|
+
try {
|
|
37
|
+
const ctx = await resolveContext(input, deps, context, true);
|
|
38
|
+
const items = await deps.engine.listDatabases(ctx);
|
|
39
|
+
return formatSuccess({
|
|
40
|
+
datasource: ctx.datasource,
|
|
41
|
+
database: ctx.database,
|
|
42
|
+
items: items.map(toPublicDatabaseInfo),
|
|
43
|
+
}, {
|
|
44
|
+
summary: `Resolved ${items.length} databases from datasource ${ctx.datasource}.`,
|
|
45
|
+
metadata: metadata(context.taskId),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return formatToolError(error, {
|
|
50
|
+
action: "list_databases",
|
|
51
|
+
metadata: metadata(context.taskId),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
export const listTablesTool = {
|
|
57
|
+
name: "list_tables",
|
|
58
|
+
description: "List tables or views in the selected database.",
|
|
59
|
+
inputSchema: contextInputShape,
|
|
60
|
+
async handler(input, deps, context) {
|
|
61
|
+
try {
|
|
62
|
+
const ctx = await resolveContext(input, deps, context, true);
|
|
63
|
+
const database = requireDatabase(input.database, ctx);
|
|
64
|
+
const items = await deps.engine.listTables(ctx, database);
|
|
65
|
+
return formatSuccess({
|
|
66
|
+
datasource: ctx.datasource,
|
|
67
|
+
database,
|
|
68
|
+
items: items.map(toPublicTableInfo),
|
|
69
|
+
}, {
|
|
70
|
+
summary: `Resolved ${items.length} tables from ${database}.`,
|
|
71
|
+
metadata: metadata(context.taskId),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return formatToolError(error, {
|
|
76
|
+
action: "list_tables",
|
|
77
|
+
metadata: metadata(context.taskId),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
export const describeTableTool = {
|
|
83
|
+
name: "describe_table",
|
|
84
|
+
description: "Describe a table, including columns, indexes, primary key, and engine hints.",
|
|
85
|
+
inputSchema: {
|
|
86
|
+
...contextInputShape,
|
|
87
|
+
table: asRequiredStringSchema("Table name to describe."),
|
|
88
|
+
},
|
|
89
|
+
async handler(input, deps, context) {
|
|
90
|
+
try {
|
|
91
|
+
const ctx = await resolveContext(input, deps, context, true);
|
|
92
|
+
const database = requireDatabase(input.database, ctx);
|
|
93
|
+
const table = asRequiredString(input.table, "table");
|
|
94
|
+
const schema = await deps.engine.describeTable(ctx, database, table);
|
|
95
|
+
return formatSuccess({
|
|
96
|
+
datasource: ctx.datasource,
|
|
97
|
+
...toPublicTableSchema(schema),
|
|
98
|
+
}, {
|
|
99
|
+
summary: `Described ${database}.${table}.`,
|
|
100
|
+
metadata: metadata(context.taskId),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
return formatToolError(error, {
|
|
105
|
+
action: "describe_table",
|
|
106
|
+
metadata: metadata(context.taskId),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
function asRequiredStringSchema(description) {
|
|
112
|
+
return z.string().trim().min(1).describe(description);
|
|
113
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ResponseMetadata, type ToolResponse } from "../utils/formatter.js";
|
|
2
|
+
export declare class ToolInputError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
type ToolErrorContext = {
|
|
6
|
+
action: string;
|
|
7
|
+
metadata: ResponseMetadata;
|
|
8
|
+
};
|
|
9
|
+
export declare function formatToolError(error: unknown, context: ToolErrorContext): ToolResponse;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { ConnectionPoolError, DatasourceResolutionError, FlashbackNoViewError, SchemaIntrospectionError, UnsupportedFeatureError, } from "taurusdb-core";
|
|
2
|
+
import { ErrorCode, formatError, } from "../utils/formatter.js";
|
|
3
|
+
export class ToolInputError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "ToolInputError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function messageOf(error) {
|
|
10
|
+
return error instanceof Error ? error.message : String(error);
|
|
11
|
+
}
|
|
12
|
+
function timeoutLikely(error) {
|
|
13
|
+
return /timeout|timed out/i.test(messageOf(error));
|
|
14
|
+
}
|
|
15
|
+
function cancelledLikely(error) {
|
|
16
|
+
return /cancelled|canceled/i.test(messageOf(error));
|
|
17
|
+
}
|
|
18
|
+
function detailsOf(error) {
|
|
19
|
+
if (!error || typeof error !== "object" || !("details" in error)) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const details = error.details;
|
|
23
|
+
return details && typeof details === "object" && !Array.isArray(details)
|
|
24
|
+
? details
|
|
25
|
+
: undefined;
|
|
26
|
+
}
|
|
27
|
+
export function formatToolError(error, context) {
|
|
28
|
+
if (error instanceof ToolInputError) {
|
|
29
|
+
return formatError({
|
|
30
|
+
code: ErrorCode.INVALID_INPUT,
|
|
31
|
+
message: error.message,
|
|
32
|
+
summary: `${context.action} failed due to invalid input.`,
|
|
33
|
+
metadata: context.metadata,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (error instanceof DatasourceResolutionError) {
|
|
37
|
+
if (error.code === "DATASOURCE_NOT_FOUND") {
|
|
38
|
+
return formatError({
|
|
39
|
+
code: ErrorCode.DATASOURCE_NOT_FOUND,
|
|
40
|
+
message: `${error.message} Verify the active datasource template or pass an explicit datasource name.`,
|
|
41
|
+
summary: `${context.action} failed because datasource could not be resolved.`,
|
|
42
|
+
metadata: context.metadata,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return formatError({
|
|
46
|
+
code: ErrorCode.INVALID_INPUT,
|
|
47
|
+
message: error.message,
|
|
48
|
+
summary: `${context.action} failed due to invalid input.`,
|
|
49
|
+
metadata: context.metadata,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (error instanceof SchemaIntrospectionError) {
|
|
53
|
+
if (error.code === "INVALID_INTROSPECTION_INPUT") {
|
|
54
|
+
return formatError({
|
|
55
|
+
code: ErrorCode.INVALID_INPUT,
|
|
56
|
+
message: error.message,
|
|
57
|
+
summary: `${context.action} failed due to invalid schema input.`,
|
|
58
|
+
metadata: context.metadata,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return formatError({
|
|
62
|
+
code: ErrorCode.CONNECTION_FAILED,
|
|
63
|
+
message: error.message,
|
|
64
|
+
summary: `${context.action} failed because schema adapter is unavailable.`,
|
|
65
|
+
metadata: context.metadata,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (error instanceof ConnectionPoolError) {
|
|
69
|
+
return formatError({
|
|
70
|
+
code: ErrorCode.CONNECTION_FAILED,
|
|
71
|
+
message: error.message,
|
|
72
|
+
summary: `${context.action} failed due to database connection issue.`,
|
|
73
|
+
metadata: context.metadata,
|
|
74
|
+
details: detailsOf(error),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (error instanceof UnsupportedFeatureError) {
|
|
78
|
+
return formatError({
|
|
79
|
+
code: ErrorCode.UNSUPPORTED_FEATURE,
|
|
80
|
+
message: error.message,
|
|
81
|
+
summary: "The requested TaurusDB feature is not available on this instance.",
|
|
82
|
+
metadata: context.metadata,
|
|
83
|
+
details: {
|
|
84
|
+
feature: error.feature,
|
|
85
|
+
required_version: error.requiredVersion,
|
|
86
|
+
current_version: error.currentVersion,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (error instanceof FlashbackNoViewError) {
|
|
91
|
+
return formatError({
|
|
92
|
+
code: ErrorCode.CONNECTION_FAILED,
|
|
93
|
+
message: error.message,
|
|
94
|
+
summary: "No historical flashback view was available for the requested timestamp.",
|
|
95
|
+
metadata: context.metadata,
|
|
96
|
+
details: detailsOf(error),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (cancelledLikely(error)) {
|
|
100
|
+
return formatError({
|
|
101
|
+
code: ErrorCode.QUERY_CANCELLED,
|
|
102
|
+
message: messageOf(error),
|
|
103
|
+
summary: `${context.action} was cancelled.`,
|
|
104
|
+
metadata: context.metadata,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (timeoutLikely(error)) {
|
|
108
|
+
return formatError({
|
|
109
|
+
code: ErrorCode.QUERY_TIMEOUT,
|
|
110
|
+
message: messageOf(error),
|
|
111
|
+
summary: `${context.action} timed out.`,
|
|
112
|
+
metadata: context.metadata,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return formatError({
|
|
116
|
+
code: ErrorCode.CONNECTION_FAILED,
|
|
117
|
+
message: messageOf(error),
|
|
118
|
+
summary: `${context.action} failed unexpectedly.`,
|
|
119
|
+
metadata: context.metadata,
|
|
120
|
+
details: detailsOf(error),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { formatSuccess } from "../utils/formatter.js";
|
|
2
|
+
const PING_DESCRIPTION = "Connectivity smoke test. Returns `pong` when the server is alive.";
|
|
3
|
+
export const pingTool = {
|
|
4
|
+
name: "ping",
|
|
5
|
+
description: PING_DESCRIPTION,
|
|
6
|
+
inputSchema: {},
|
|
7
|
+
async handler(_input, deps, context) {
|
|
8
|
+
return formatSuccess({ value: deps.pingResponse }, {
|
|
9
|
+
summary: deps.pingResponse,
|
|
10
|
+
metadata: { task_id: context.taskId },
|
|
11
|
+
});
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatSuccess } from "../utils/formatter.js";
|
|
3
|
+
import { formatToolError, ToolInputError } from "./error-handling.js";
|
|
4
|
+
import { asOptionalBoolean, asOptionalPositiveInteger, asOptionalString, contextInputShape, metadata, resolveContext, toPublicQueryResult, } from "./common.js";
|
|
5
|
+
function summarizeProcesslistRows(rowCount, truncated) {
|
|
6
|
+
if (rowCount === 1) {
|
|
7
|
+
return truncated
|
|
8
|
+
? "Returned 1 processlist row (truncated)."
|
|
9
|
+
: "Returned 1 processlist row.";
|
|
10
|
+
}
|
|
11
|
+
return truncated
|
|
12
|
+
? `Returned ${rowCount} processlist rows (truncated).`
|
|
13
|
+
: `Returned ${rowCount} processlist rows.`;
|
|
14
|
+
}
|
|
15
|
+
export const showProcesslistTool = {
|
|
16
|
+
name: "show_processlist",
|
|
17
|
+
description: "Show current MySQL processlist rows with safe defaults for connection and lock troubleshooting.",
|
|
18
|
+
inputSchema: {
|
|
19
|
+
...contextInputShape,
|
|
20
|
+
user: optionalStringSchema("Optional user to focus on."),
|
|
21
|
+
host: optionalStringSchema("Optional host or client IP prefix to focus on."),
|
|
22
|
+
session_database: optionalStringSchema("Optional processlist DB value to focus on."),
|
|
23
|
+
command: optionalStringSchema("Optional command to focus on, such as Query or Sleep."),
|
|
24
|
+
min_time_seconds: nonNegativeIntegerSchema("Only include rows whose TIME value is at least this many seconds.").optional(),
|
|
25
|
+
max_rows: positiveIntegerSchema("Maximum number of processlist rows to return.")
|
|
26
|
+
.max(100)
|
|
27
|
+
.optional()
|
|
28
|
+
.default(20),
|
|
29
|
+
include_idle: z
|
|
30
|
+
.boolean()
|
|
31
|
+
.optional()
|
|
32
|
+
.default(false)
|
|
33
|
+
.describe("Whether to include Sleep sessions."),
|
|
34
|
+
include_system: z
|
|
35
|
+
.boolean()
|
|
36
|
+
.optional()
|
|
37
|
+
.default(false)
|
|
38
|
+
.describe("Whether to include system user sessions."),
|
|
39
|
+
include_info: z
|
|
40
|
+
.boolean()
|
|
41
|
+
.optional()
|
|
42
|
+
.default(false)
|
|
43
|
+
.describe("Whether to include truncated SQL text from the INFO column."),
|
|
44
|
+
info_max_chars: positiveIntegerSchema("Maximum length of INFO text to return when include_info is true.")
|
|
45
|
+
.max(2048)
|
|
46
|
+
.optional()
|
|
47
|
+
.default(256),
|
|
48
|
+
},
|
|
49
|
+
async handler(input, deps, context) {
|
|
50
|
+
try {
|
|
51
|
+
const ctx = await resolveContext(input, deps, context, true);
|
|
52
|
+
const user = asOptionalString(input.user, "user");
|
|
53
|
+
const host = asOptionalString(input.host, "host");
|
|
54
|
+
const sessionDatabase = asOptionalString(input.session_database, "session_database");
|
|
55
|
+
const command = asOptionalString(input.command, "command");
|
|
56
|
+
const minTimeSeconds = asOptionalNonNegativeInteger(input.min_time_seconds, "min_time_seconds") ?? 0;
|
|
57
|
+
const maxRows = asOptionalPositiveInteger(input.max_rows, "max_rows") ?? 20;
|
|
58
|
+
const includeIdle = asOptionalBoolean(input.include_idle, "include_idle") ?? false;
|
|
59
|
+
const includeSystem = asOptionalBoolean(input.include_system, "include_system") ?? false;
|
|
60
|
+
const includeInfo = asOptionalBoolean(input.include_info, "include_info") ?? false;
|
|
61
|
+
const infoMaxChars = asOptionalPositiveInteger(input.info_max_chars, "info_max_chars") ??
|
|
62
|
+
256;
|
|
63
|
+
const result = await deps.engine.showProcesslist({
|
|
64
|
+
user,
|
|
65
|
+
host,
|
|
66
|
+
sessionDatabase,
|
|
67
|
+
command,
|
|
68
|
+
minTimeSeconds,
|
|
69
|
+
maxRows,
|
|
70
|
+
includeIdle,
|
|
71
|
+
includeSystem,
|
|
72
|
+
includeInfo,
|
|
73
|
+
infoMaxChars,
|
|
74
|
+
}, ctx);
|
|
75
|
+
return formatSuccess({
|
|
76
|
+
datasource: ctx.datasource,
|
|
77
|
+
filters: {
|
|
78
|
+
user,
|
|
79
|
+
host,
|
|
80
|
+
session_database: sessionDatabase,
|
|
81
|
+
command,
|
|
82
|
+
min_time_seconds: minTimeSeconds || undefined,
|
|
83
|
+
include_idle: includeIdle,
|
|
84
|
+
include_system: includeSystem,
|
|
85
|
+
include_info: includeInfo,
|
|
86
|
+
max_rows: maxRows,
|
|
87
|
+
},
|
|
88
|
+
...toPublicQueryResult(result),
|
|
89
|
+
}, {
|
|
90
|
+
summary: summarizeProcesslistRows(result.rowCount, result.truncated),
|
|
91
|
+
metadata: metadata(context.taskId, {
|
|
92
|
+
duration_ms: result.durationMs,
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
return formatToolError(error, {
|
|
98
|
+
action: "show_processlist",
|
|
99
|
+
metadata: metadata(context.taskId),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
function optionalStringSchema(description) {
|
|
105
|
+
return z.string().trim().min(1).optional().describe(description);
|
|
106
|
+
}
|
|
107
|
+
function positiveIntegerSchema(description) {
|
|
108
|
+
return z.number().int().positive().describe(description);
|
|
109
|
+
}
|
|
110
|
+
function nonNegativeIntegerSchema(description) {
|
|
111
|
+
return z.number().int().nonnegative().describe(description);
|
|
112
|
+
}
|
|
113
|
+
function asOptionalNonNegativeInteger(value, fieldName) {
|
|
114
|
+
if (value === undefined) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
118
|
+
throw new ToolInputError(`Invalid ${fieldName}: expected a non-negative integer.`);
|
|
119
|
+
}
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ErrorCode, formatBlocked, formatConfirmationRequired, formatError, formatSuccess } from "../utils/formatter.js";
|
|
3
|
+
import { formatToolError } from "./error-handling.js";
|
|
4
|
+
import { asOptionalString, asRequiredString, contextInputShape, metadata, resolveContext, statementTypeFromSql, summarizeMutation, summarizeRows, toPublicExplainResult, toPublicMutationResult, toPublicQueryResult, } from "./common.js";
|
|
5
|
+
function blockedReason(decision) {
|
|
6
|
+
return decision.riskHints[0] ?? "The SQL statement is blocked by safety policy.";
|
|
7
|
+
}
|
|
8
|
+
async function inspectSql(toolName, sql, ctx, deps) {
|
|
9
|
+
return deps.engine.inspectSql({
|
|
10
|
+
toolName,
|
|
11
|
+
sql,
|
|
12
|
+
context: ctx,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async function ensureConfirmation(sql, decision, ctx, deps, taskId, confirmationToken) {
|
|
16
|
+
if (!decision.requiresConfirmation) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const responseMetadata = metadata(taskId, {
|
|
20
|
+
sql_hash: decision.sqlHash,
|
|
21
|
+
statement_type: statementTypeFromSql(sql),
|
|
22
|
+
});
|
|
23
|
+
if (confirmationToken) {
|
|
24
|
+
const validation = await deps.engine.validateConfirmation(confirmationToken, sql, ctx);
|
|
25
|
+
if (validation.valid) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return formatError({
|
|
29
|
+
code: ErrorCode.CONFIRMATION_INVALID,
|
|
30
|
+
message: validation.reason ?? "Confirmation token validation failed.",
|
|
31
|
+
summary: "The provided confirmation token is invalid for this SQL statement.",
|
|
32
|
+
metadata: responseMetadata,
|
|
33
|
+
details: {
|
|
34
|
+
reason_codes: validation.reasonCodes,
|
|
35
|
+
risk_hints: validation.riskHints,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const outcome = await deps.engine.handleConfirmation(decision, ctx);
|
|
40
|
+
if (outcome.status === "token_issued") {
|
|
41
|
+
return formatConfirmationRequired({
|
|
42
|
+
confirmationToken: outcome.token,
|
|
43
|
+
metadata: responseMetadata,
|
|
44
|
+
riskLevel: decision.riskLevel,
|
|
45
|
+
sqlHash: decision.sqlHash,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
export const executeReadonlySqlTool = {
|
|
51
|
+
name: "execute_readonly_sql",
|
|
52
|
+
description: "Execute readonly SQL such as SELECT, SHOW, EXPLAIN, or DESCRIBE with guardrail enforcement.",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
...contextInputShape,
|
|
55
|
+
sql: requiredSqlSchema("Readonly SQL to execute."),
|
|
56
|
+
confirmation_token: optionalTokenSchema(),
|
|
57
|
+
},
|
|
58
|
+
async handler(input, deps, context) {
|
|
59
|
+
const sql = asRequiredString(input.sql, "sql");
|
|
60
|
+
const statementType = statementTypeFromSql(sql);
|
|
61
|
+
try {
|
|
62
|
+
const ctx = await resolveContext(input, deps, context, true);
|
|
63
|
+
const decision = await inspectSql("execute_readonly_sql", sql, ctx, deps);
|
|
64
|
+
const baseMetadata = metadata(context.taskId, {
|
|
65
|
+
sql_hash: decision.sqlHash,
|
|
66
|
+
statement_type: statementType,
|
|
67
|
+
});
|
|
68
|
+
if (decision.action === "block") {
|
|
69
|
+
return formatBlocked({
|
|
70
|
+
reason: blockedReason(decision),
|
|
71
|
+
metadata: baseMetadata,
|
|
72
|
+
details: {
|
|
73
|
+
risk_level: decision.riskLevel,
|
|
74
|
+
reason_codes: decision.reasonCodes,
|
|
75
|
+
risk_hints: decision.riskHints,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const confirmationResponse = await ensureConfirmation(sql, decision, ctx, deps, context.taskId, asOptionalString(input.confirmation_token, "confirmation_token"));
|
|
80
|
+
if (confirmationResponse) {
|
|
81
|
+
return confirmationResponse;
|
|
82
|
+
}
|
|
83
|
+
const result = await deps.engine.executeReadonly(sql, ctx, {
|
|
84
|
+
timeoutMs: decision.runtimeLimits.timeoutMs,
|
|
85
|
+
maxRows: decision.runtimeLimits.maxRows,
|
|
86
|
+
maxColumns: decision.runtimeLimits.maxColumns,
|
|
87
|
+
maxFieldChars: decision.runtimeLimits.maxFieldChars,
|
|
88
|
+
});
|
|
89
|
+
return formatSuccess(toPublicQueryResult(result), {
|
|
90
|
+
summary: summarizeRows(result.rowCount, result.truncated),
|
|
91
|
+
metadata: metadata(context.taskId, {
|
|
92
|
+
sql_hash: decision.sqlHash,
|
|
93
|
+
statement_type: statementType,
|
|
94
|
+
duration_ms: result.durationMs,
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return formatToolError(error, {
|
|
100
|
+
action: "execute_readonly_sql",
|
|
101
|
+
metadata: metadata(context.taskId, {
|
|
102
|
+
statement_type: statementType,
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
export const explainSqlTool = {
|
|
109
|
+
name: "explain_sql",
|
|
110
|
+
description: "Run EXPLAIN for a SQL statement and return plan analysis together with guardrail hints.",
|
|
111
|
+
inputSchema: {
|
|
112
|
+
...contextInputShape,
|
|
113
|
+
sql: requiredSqlSchema("SQL statement to analyze with EXPLAIN."),
|
|
114
|
+
},
|
|
115
|
+
async handler(input, deps, context) {
|
|
116
|
+
const sql = asRequiredString(input.sql, "sql");
|
|
117
|
+
const statementType = statementTypeFromSql(sql);
|
|
118
|
+
try {
|
|
119
|
+
const ctx = await resolveContext(input, deps, context, true);
|
|
120
|
+
const decision = await inspectSql("explain_sql", sql, ctx, deps);
|
|
121
|
+
const baseMetadata = metadata(context.taskId, {
|
|
122
|
+
sql_hash: decision.sqlHash,
|
|
123
|
+
statement_type: statementType,
|
|
124
|
+
});
|
|
125
|
+
if (decision.action === "block") {
|
|
126
|
+
return formatBlocked({
|
|
127
|
+
reason: blockedReason(decision),
|
|
128
|
+
metadata: baseMetadata,
|
|
129
|
+
details: {
|
|
130
|
+
risk_level: decision.riskLevel,
|
|
131
|
+
reason_codes: decision.reasonCodes,
|
|
132
|
+
risk_hints: decision.riskHints,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const result = await deps.engine.explain(sql, ctx);
|
|
137
|
+
return formatSuccess(toPublicExplainResult(result, decision), {
|
|
138
|
+
summary: decision.requiresConfirmation
|
|
139
|
+
? "Explain generated. Executing this SQL would require explicit confirmation."
|
|
140
|
+
: "Explain generated.",
|
|
141
|
+
metadata: metadata(context.taskId, {
|
|
142
|
+
sql_hash: decision.sqlHash,
|
|
143
|
+
statement_type: statementType,
|
|
144
|
+
duration_ms: result.durationMs,
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
return formatToolError(error, {
|
|
150
|
+
action: "explain_sql",
|
|
151
|
+
metadata: metadata(context.taskId, {
|
|
152
|
+
statement_type: statementType,
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
export const executeSqlTool = {
|
|
159
|
+
name: "execute_sql",
|
|
160
|
+
description: "Execute mutation SQL such as INSERT, UPDATE, or DELETE when mutations are explicitly enabled.",
|
|
161
|
+
inputSchema: {
|
|
162
|
+
...contextInputShape,
|
|
163
|
+
sql: requiredSqlSchema("Mutation SQL to execute."),
|
|
164
|
+
confirmation_token: optionalTokenSchema(),
|
|
165
|
+
},
|
|
166
|
+
exposeWhen: (config) => config.enableMutations,
|
|
167
|
+
async handler(input, deps, context) {
|
|
168
|
+
const sql = asRequiredString(input.sql, "sql");
|
|
169
|
+
const statementType = statementTypeFromSql(sql);
|
|
170
|
+
try {
|
|
171
|
+
const ctx = await resolveContext(input, deps, context, false);
|
|
172
|
+
const decision = await inspectSql("execute_sql", sql, ctx, deps);
|
|
173
|
+
const baseMetadata = metadata(context.taskId, {
|
|
174
|
+
sql_hash: decision.sqlHash,
|
|
175
|
+
statement_type: statementType,
|
|
176
|
+
});
|
|
177
|
+
if (decision.action === "block") {
|
|
178
|
+
return formatBlocked({
|
|
179
|
+
reason: blockedReason(decision),
|
|
180
|
+
metadata: baseMetadata,
|
|
181
|
+
details: {
|
|
182
|
+
risk_level: decision.riskLevel,
|
|
183
|
+
reason_codes: decision.reasonCodes,
|
|
184
|
+
risk_hints: decision.riskHints,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const confirmationResponse = await ensureConfirmation(sql, decision, ctx, deps, context.taskId, asOptionalString(input.confirmation_token, "confirmation_token"));
|
|
189
|
+
if (confirmationResponse) {
|
|
190
|
+
return confirmationResponse;
|
|
191
|
+
}
|
|
192
|
+
const result = await deps.engine.executeMutation(sql, ctx, {
|
|
193
|
+
timeoutMs: decision.runtimeLimits.timeoutMs,
|
|
194
|
+
});
|
|
195
|
+
return formatSuccess(toPublicMutationResult(result), {
|
|
196
|
+
summary: summarizeMutation(result.affectedRows),
|
|
197
|
+
metadata: metadata(context.taskId, {
|
|
198
|
+
sql_hash: decision.sqlHash,
|
|
199
|
+
statement_type: statementType,
|
|
200
|
+
duration_ms: result.durationMs,
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
return formatToolError(error, {
|
|
206
|
+
action: "execute_sql",
|
|
207
|
+
metadata: metadata(context.taskId, {
|
|
208
|
+
statement_type: statementType,
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
function requiredSqlSchema(description) {
|
|
215
|
+
return z.string().trim().min(1).describe(description);
|
|
216
|
+
}
|
|
217
|
+
function optionalTokenSchema() {
|
|
218
|
+
return z
|
|
219
|
+
.string()
|
|
220
|
+
.trim()
|
|
221
|
+
.min(1)
|
|
222
|
+
.optional()
|
|
223
|
+
.describe("Confirmation token returned by a previous guarded call when required.");
|
|
224
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type FeatureMatrix, type KernelInfo, type Config } from "taurusdb-core";
|
|
3
|
+
import type { ServerDeps } from "../server.js";
|
|
4
|
+
import { type ToolResponse } from "../utils/formatter.js";
|
|
5
|
+
export type ToolDeps = ServerDeps;
|
|
6
|
+
export interface ToolRegistrationProbe {
|
|
7
|
+
kernelInfo?: KernelInfo;
|
|
8
|
+
features?: FeatureMatrix;
|
|
9
|
+
}
|
|
10
|
+
export type ToolInvokeContext = {
|
|
11
|
+
taskId: string;
|
|
12
|
+
};
|
|
13
|
+
export interface ToolDefinition<I extends Record<string, unknown> = Record<string, unknown>, O = unknown> {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
inputSchema: Record<string, unknown>;
|
|
17
|
+
handler: (input: I, deps: ToolDeps, context: ToolInvokeContext) => Promise<ToolResponse<O>>;
|
|
18
|
+
exposeWhen?: (config: Config) => boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare const commonToolDefinitions: ToolDefinition[];
|
|
21
|
+
export declare const capabilityToolDefinitions: ToolDefinition[];
|
|
22
|
+
export declare function registerTools(server: McpServer, deps: ToolDeps, config: Config, probeOrTools?: ToolRegistrationProbe | ToolDefinition[], maybeTools?: ToolDefinition[]): void;
|