mcp-data-agent 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/.env.example +18 -0
- package/.idea/jsLibraryMappings.xml +6 -0
- package/.idea/mcp_engine.iml +8 -0
- package/.idea/modules.xml +8 -0
- package/.idea/php.xml +19 -0
- package/.idea/vcs.xml +6 -0
- package/README.md +587 -0
- package/examples/basic/package.json +26 -0
- package/examples/basic/src/index.ts +107 -0
- package/examples/basic/tsconfig.json +15 -0
- package/package.json +106 -0
- package/packages/core/README.md +7 -0
- package/packages/core/package.json +38 -0
- package/packages/core/src/agent.test.ts +213 -0
- package/packages/core/src/agent.ts +373 -0
- package/packages/core/src/errors.ts +73 -0
- package/packages/core/src/index.ts +78 -0
- package/packages/core/src/permissions.ts +71 -0
- package/packages/core/src/query-safety.ts +125 -0
- package/packages/core/src/schema-safety.ts +118 -0
- package/packages/core/src/tenant.ts +58 -0
- package/packages/core/src/tool-registry.ts +61 -0
- package/packages/core/src/types.ts +250 -0
- package/packages/core/tsconfig.json +10 -0
- package/packages/mcp/README.md +7 -0
- package/packages/mcp/package.json +46 -0
- package/packages/mcp/src/index.ts +15 -0
- package/packages/mcp/src/server.ts +170 -0
- package/packages/mcp/tsconfig.json +11 -0
- package/packages/postgres/README.md +7 -0
- package/packages/postgres/package.json +46 -0
- package/packages/postgres/src/adapter.ts +358 -0
- package/packages/postgres/src/index.ts +19 -0
- package/packages/postgres/tsconfig.json +11 -0
- package/tsconfig.base.json +22 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Data Agent — @mcp-data-agent/core
|
|
3
|
+
* Created by Arslan Habib
|
|
4
|
+
*
|
|
5
|
+
* ---------------------------------------------------------------------------
|
|
6
|
+
* FILE: query-safety.ts
|
|
7
|
+
* PURPOSE:
|
|
8
|
+
* Defense-in-depth SQL validation before any query reaches a database
|
|
9
|
+
* adapter. Blocks writes, multi-statements, locking clauses, and caps
|
|
10
|
+
* result size / timeout.
|
|
11
|
+
*
|
|
12
|
+
* IMPORTANT:
|
|
13
|
+
* Regex validation is NOT enough alone. Host apps should also:
|
|
14
|
+
* - Use read-only DB credentials for AI analytics
|
|
15
|
+
* - Use bound parameters (never string-concat AI text into SQL)
|
|
16
|
+
* - Enforce tenant isolation at the DB (e.g. Postgres RLS)
|
|
17
|
+
* ---------------------------------------------------------------------------
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { QueryValidationError } from "./errors.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Keywords that indicate a mutating or dangerous statement.
|
|
24
|
+
* Matched as whole words (case-insensitive).
|
|
25
|
+
*/
|
|
26
|
+
const FORBIDDEN_KEYWORDS =
|
|
27
|
+
/\b(insert|update|delete|drop|alter|truncate|create|grant|revoke|copy|call|execute|merge|replace|attach|detach|pragma|vacuum|reindex|cluster|comment|security|owner|load|import|export|into\s+outfile|load_file)\b/i;
|
|
28
|
+
|
|
29
|
+
/** Detects a second statement after a semicolon (SQL injection pattern). */
|
|
30
|
+
const MULTI_STATEMENT = /;\s*\S/;
|
|
31
|
+
|
|
32
|
+
/** Result of a successful validation pass. */
|
|
33
|
+
export interface ValidatedQuery {
|
|
34
|
+
sql: string;
|
|
35
|
+
limit: number;
|
|
36
|
+
timeoutMs: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Inputs accepted by validateReadOnlyQuery(). */
|
|
40
|
+
export interface ValidateQueryOptions {
|
|
41
|
+
sql: string;
|
|
42
|
+
limit?: number;
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
defaultLimit?: number;
|
|
45
|
+
defaultTimeoutMs?: number;
|
|
46
|
+
maxLimit?: number;
|
|
47
|
+
maxTimeoutMs?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Validate that a query is a single read-only SELECT (or WITH … SELECT).
|
|
52
|
+
* Throws QueryValidationError on anything unsafe.
|
|
53
|
+
*/
|
|
54
|
+
export function validateReadOnlyQuery(
|
|
55
|
+
options: ValidateQueryOptions,
|
|
56
|
+
): ValidatedQuery {
|
|
57
|
+
const raw = options.sql?.trim() ?? "";
|
|
58
|
+
if (!raw) {
|
|
59
|
+
throw new QueryValidationError("Query SQL is required");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Block "SELECT 1; DROP TABLE ..." style attacks
|
|
63
|
+
if (MULTI_STATEMENT.test(raw)) {
|
|
64
|
+
throw new QueryValidationError("Multiple SQL statements are not allowed");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Allow a single trailing semicolon; strip it for further checks
|
|
68
|
+
const sql = raw.replace(/;\s*$/, "");
|
|
69
|
+
|
|
70
|
+
if (FORBIDDEN_KEYWORDS.test(sql)) {
|
|
71
|
+
throw new QueryValidationError(
|
|
72
|
+
"Only read-only SELECT queries are allowed by default",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Must start with SELECT or WITH (common table expressions)
|
|
77
|
+
const normalized = sql.replace(/^\s*\(/, "").trimStart();
|
|
78
|
+
const isSelect =
|
|
79
|
+
/^select\b/i.test(normalized) ||
|
|
80
|
+
/^with\b/i.test(normalized);
|
|
81
|
+
|
|
82
|
+
if (!isSelect) {
|
|
83
|
+
throw new QueryValidationError(
|
|
84
|
+
"Query must be a SELECT or WITH (CTE) statement",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// SELECT INTO can create tables — blocked in Phase 1
|
|
89
|
+
if (/\binto\b/i.test(sql) && !/\binto\s+temp(orary)?\b/i.test(sql)) {
|
|
90
|
+
throw new QueryValidationError("SELECT INTO is not allowed");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Row locks are not needed for analytics and can block writers
|
|
94
|
+
if (/\bfor\s+update\b/i.test(sql) || /\block\s+in\s+share\b/i.test(sql)) {
|
|
95
|
+
throw new QueryValidationError("Locking clauses are not allowed");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Clamp limit and timeout to safe ranges
|
|
99
|
+
const maxLimit = options.maxLimit ?? 1000;
|
|
100
|
+
const maxTimeout = options.maxTimeoutMs ?? 30_000;
|
|
101
|
+
const defaultLimit = options.defaultLimit ?? 100;
|
|
102
|
+
const defaultTimeout = options.defaultTimeoutMs ?? 10_000;
|
|
103
|
+
|
|
104
|
+
const limit = Math.min(
|
|
105
|
+
Math.max(1, options.limit ?? defaultLimit),
|
|
106
|
+
maxLimit,
|
|
107
|
+
);
|
|
108
|
+
const timeoutMs = Math.min(
|
|
109
|
+
Math.max(100, options.timeoutMs ?? defaultTimeout),
|
|
110
|
+
maxTimeout,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return { sql, limit, timeoutMs };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Ensure a LIMIT clause exists without rewriting complex SQL aggressively.
|
|
118
|
+
* If the query already has LIMIT, leave it; otherwise append one.
|
|
119
|
+
*/
|
|
120
|
+
export function ensureLimit(sql: string, limit: number): string {
|
|
121
|
+
if (/\blimit\s+\d+/i.test(sql)) {
|
|
122
|
+
return sql;
|
|
123
|
+
}
|
|
124
|
+
return `${sql} LIMIT ${limit}`;
|
|
125
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Data Agent — @mcp-data-agent/core
|
|
3
|
+
* Created by Arslan Habib
|
|
4
|
+
*
|
|
5
|
+
* ---------------------------------------------------------------------------
|
|
6
|
+
* FILE: schema-safety.ts
|
|
7
|
+
* PURPOSE:
|
|
8
|
+
* Prevents sensitive columns (passwords, tokens, salaries, etc.) from
|
|
9
|
+
* reaching the LLM. Works in two stages:
|
|
10
|
+
* 1) mark columns as sensitive (patterns + developer protectField list)
|
|
11
|
+
* 2) redact them from schema and from query result rows
|
|
12
|
+
*
|
|
13
|
+
* WHY:
|
|
14
|
+
* Even a valid SELECT might return password_hash. We strip those keys
|
|
15
|
+
* before returning data to AI tools.
|
|
16
|
+
* ---------------------------------------------------------------------------
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type {
|
|
20
|
+
ColumnSchema,
|
|
21
|
+
DatabaseSchema,
|
|
22
|
+
ProtectFieldOptions,
|
|
23
|
+
TableSchema,
|
|
24
|
+
} from "./types.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Heuristic name patterns treated as sensitive by default.
|
|
28
|
+
* Developers can add more via agent.protectField().
|
|
29
|
+
*/
|
|
30
|
+
const DEFAULT_SENSITIVE_PATTERNS = [
|
|
31
|
+
/password/i,
|
|
32
|
+
/passwd/i,
|
|
33
|
+
/secret/i,
|
|
34
|
+
/api[_-]?key/i,
|
|
35
|
+
/access[_-]?token/i,
|
|
36
|
+
/refresh[_-]?token/i,
|
|
37
|
+
/private[_-]?key/i,
|
|
38
|
+
/credit[_-]?card/i,
|
|
39
|
+
/card[_-]?number/i,
|
|
40
|
+
/ssn/i,
|
|
41
|
+
/salary/i,
|
|
42
|
+
/password_hash/i,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** Builds a stable key: "schema.table.column" (lowercased). */
|
|
46
|
+
function fieldKey(schema: string | undefined, table: string, column: string): string {
|
|
47
|
+
return `${schema ?? "public"}.${table}.${column}`.toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Returns true if the column name looks sensitive based on default patterns. */
|
|
51
|
+
export function isLikelySensitiveColumn(columnName: string): boolean {
|
|
52
|
+
return DEFAULT_SENSITIVE_PATTERNS.some((pattern) => pattern.test(columnName));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Converts protectField() options into a Set for O(1) lookups. */
|
|
56
|
+
export function buildProtectedFieldSet(
|
|
57
|
+
fields: ProtectFieldOptions[] = [],
|
|
58
|
+
): Set<string> {
|
|
59
|
+
return new Set(
|
|
60
|
+
fields.map((f) => fieldKey(f.schema, f.table, f.column)),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Walks the schema and sets `sensitive: true` on columns that match
|
|
66
|
+
* patterns or the developer's protected-field list.
|
|
67
|
+
*/
|
|
68
|
+
export function markSensitiveColumns(
|
|
69
|
+
schema: DatabaseSchema,
|
|
70
|
+
protectedFields: Set<string>,
|
|
71
|
+
): DatabaseSchema {
|
|
72
|
+
const tables = schema.tables.map((table) => {
|
|
73
|
+
const columns = table.columns.map((col) => {
|
|
74
|
+
const key = fieldKey(table.schema, table.name, col.name);
|
|
75
|
+
const sensitive =
|
|
76
|
+
col.sensitive === true ||
|
|
77
|
+
protectedFields.has(key) ||
|
|
78
|
+
isLikelySensitiveColumn(col.name);
|
|
79
|
+
return sensitive ? { ...col, sensitive: true } : col;
|
|
80
|
+
});
|
|
81
|
+
return { ...table, columns };
|
|
82
|
+
});
|
|
83
|
+
return { ...schema, tables };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Returns a copy of the schema with sensitive columns removed entirely.
|
|
88
|
+
* This is what database.schema returns to the AI.
|
|
89
|
+
*/
|
|
90
|
+
export function redactSchemaForAi(schema: DatabaseSchema): DatabaseSchema {
|
|
91
|
+
const tables: TableSchema[] = schema.tables.map((table) => {
|
|
92
|
+
const columns: ColumnSchema[] = table.columns.filter((c) => !c.sensitive);
|
|
93
|
+
return { ...table, columns };
|
|
94
|
+
});
|
|
95
|
+
return { ...schema, tables };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Removes sensitive keys from each result row (by column name, case-insensitive).
|
|
100
|
+
* Applied after database.query so leaked SELECT columns never reach the LLM.
|
|
101
|
+
*/
|
|
102
|
+
export function redactResultRows(
|
|
103
|
+
rows: Record<string, unknown>[],
|
|
104
|
+
sensitiveColumnNames: Set<string>,
|
|
105
|
+
): Record<string, unknown>[] {
|
|
106
|
+
if (sensitiveColumnNames.size === 0) {
|
|
107
|
+
return rows;
|
|
108
|
+
}
|
|
109
|
+
return rows.map((row) => {
|
|
110
|
+
const next: Record<string, unknown> = {};
|
|
111
|
+
for (const [key, value] of Object.entries(row)) {
|
|
112
|
+
if (!sensitiveColumnNames.has(key.toLowerCase())) {
|
|
113
|
+
next[key] = value;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return next;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Data Agent — @mcp-data-agent/core
|
|
3
|
+
* Created by Arslan Habib
|
|
4
|
+
*
|
|
5
|
+
* ---------------------------------------------------------------------------
|
|
6
|
+
* FILE: tenant.ts
|
|
7
|
+
* PURPOSE:
|
|
8
|
+
* Multi-tenant safety helpers. SaaS apps isolate data by tenant_id; this
|
|
9
|
+
* module ensures tenant context is present and that AI/tool arguments
|
|
10
|
+
* cannot spoof or override which tenant is in scope.
|
|
11
|
+
*
|
|
12
|
+
* RULE:
|
|
13
|
+
* tenantId comes ONLY from the authenticated application session.
|
|
14
|
+
* ---------------------------------------------------------------------------
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { TenantIsolationError } from "./errors.js";
|
|
18
|
+
import type { TenantContext } from "./types.js";
|
|
19
|
+
|
|
20
|
+
/** Default column name used by many SaaS schemas for row-level tenancy. */
|
|
21
|
+
export const DEFAULT_TENANT_COLUMN = "tenant_id";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validates that tenant.tenantId is a non-empty string.
|
|
25
|
+
* Called on every tool invocation via MCPDataAgent.getContext().
|
|
26
|
+
*/
|
|
27
|
+
export function assertTenantContext(tenant: TenantContext): void {
|
|
28
|
+
if (!tenant.tenantId || typeof tenant.tenantId !== "string") {
|
|
29
|
+
throw new TenantIsolationError(
|
|
30
|
+
"tenantId must be derived from the authenticated session",
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (tenant.tenantId.trim().length === 0) {
|
|
34
|
+
throw new TenantIsolationError("tenantId cannot be empty");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Returns the configured tenant column name, or the default "tenant_id". */
|
|
39
|
+
export function getTenantColumn(tenant: TenantContext): string {
|
|
40
|
+
return tenant.tenantColumn ?? DEFAULT_TENANT_COLUMN;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reject AI/tool attempts to override tenant identity via parameters.
|
|
45
|
+
* Example attack: { sql: "...", tenantId: "other_gym" } — this throws.
|
|
46
|
+
*/
|
|
47
|
+
export function rejectTenantOverride(
|
|
48
|
+
params: Record<string, unknown>,
|
|
49
|
+
reservedKeys: string[] = ["tenantId", "tenant_id", "tenant"],
|
|
50
|
+
): void {
|
|
51
|
+
for (const key of reservedKeys) {
|
|
52
|
+
if (Object.prototype.hasOwnProperty.call(params, key)) {
|
|
53
|
+
throw new TenantIsolationError(
|
|
54
|
+
`Parameter "${key}" is not allowed; tenant context comes from the session`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Data Agent — @mcp-data-agent/core
|
|
3
|
+
* Created by Arslan Habib
|
|
4
|
+
*
|
|
5
|
+
* ---------------------------------------------------------------------------
|
|
6
|
+
* FILE: tool-registry.ts
|
|
7
|
+
* PURPOSE:
|
|
8
|
+
* In-memory registry of tools the agent can invoke. Built-in tools
|
|
9
|
+
* (database.schema, database.query) and custom tools (members.search, …)
|
|
10
|
+
* are stored here by unique name.
|
|
11
|
+
*
|
|
12
|
+
* RESPONSIBILITIES:
|
|
13
|
+
* - Prevent duplicate tool names
|
|
14
|
+
* - Reject mutating tools that misuse the read-only database.query permission
|
|
15
|
+
* ---------------------------------------------------------------------------
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { ConfigurationError } from "./errors.js";
|
|
19
|
+
import type { ToolDefinition } from "./types.js";
|
|
20
|
+
|
|
21
|
+
/** Map of tool name → ToolDefinition. Owned by MCPDataAgent. */
|
|
22
|
+
export class ToolRegistry {
|
|
23
|
+
private readonly tools = new Map<string, ToolDefinition>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Registers a new tool. Throws if the name is missing or already taken.
|
|
27
|
+
* Mutating tools must use an explicit write permission (not database.query).
|
|
28
|
+
*/
|
|
29
|
+
register<TInput = unknown, TOutput = unknown>(
|
|
30
|
+
tool: ToolDefinition<TInput, TOutput>,
|
|
31
|
+
): void {
|
|
32
|
+
if (!tool.name?.trim()) {
|
|
33
|
+
throw new ConfigurationError("Tool name is required");
|
|
34
|
+
}
|
|
35
|
+
if (this.tools.has(tool.name)) {
|
|
36
|
+
throw new ConfigurationError(`Tool already registered: ${tool.name}`);
|
|
37
|
+
}
|
|
38
|
+
// database.query is reserved for read-only analytics
|
|
39
|
+
if (tool.mutates && tool.permission === "database.query") {
|
|
40
|
+
throw new ConfigurationError(
|
|
41
|
+
"Mutating tools must use an explicit write permission, not database.query",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
this.tools.set(tool.name, tool as ToolDefinition);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Lookup a single tool by name (undefined if not registered). */
|
|
48
|
+
get(name: string): ToolDefinition | undefined {
|
|
49
|
+
return this.tools.get(name);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** All registered tools (used by MCP bridge to expose custom tools). */
|
|
53
|
+
list(): ToolDefinition[] {
|
|
54
|
+
return [...this.tools.values()];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Returns true if a tool with this name exists. */
|
|
58
|
+
has(name: string): boolean {
|
|
59
|
+
return this.tools.has(name);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Data Agent — @mcp-data-agent/core
|
|
3
|
+
* Created by Arslan Habib
|
|
4
|
+
*
|
|
5
|
+
* ---------------------------------------------------------------------------
|
|
6
|
+
* FILE: types.ts
|
|
7
|
+
* PURPOSE:
|
|
8
|
+
* Shared TypeScript contracts for the whole framework. Every package
|
|
9
|
+
* (postgres, mcp, host apps) depends on these shapes so adapters, tools,
|
|
10
|
+
* and the agent speak the same language.
|
|
11
|
+
*
|
|
12
|
+
* WHAT LIVES HERE:
|
|
13
|
+
* - Auth / tenant / request context (who is calling, which tenant)
|
|
14
|
+
* - Database schema + query request/result types
|
|
15
|
+
* - DatabaseAdapter interface (what every DB driver must implement)
|
|
16
|
+
* - Tool definitions, audit events, entities, agent options
|
|
17
|
+
*
|
|
18
|
+
* SECURITY NOTES:
|
|
19
|
+
* - AuthContext and TenantContext must come from the host session.
|
|
20
|
+
* - Never populate tenantId from AI/tool arguments.
|
|
21
|
+
* ---------------------------------------------------------------------------
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Permission string, e.g. "members.read" or "database.query". */
|
|
25
|
+
export type Permission = string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Who is authenticated for this request.
|
|
29
|
+
* Provided by the host application after it verifies the user session.
|
|
30
|
+
*/
|
|
31
|
+
export interface AuthContext {
|
|
32
|
+
/** Authenticated application user id — never trust AI-supplied values. */
|
|
33
|
+
userId: string;
|
|
34
|
+
/** Optional display name for audit logs. */
|
|
35
|
+
displayName?: string;
|
|
36
|
+
/** Roles assigned by the host application. */
|
|
37
|
+
roles?: string[];
|
|
38
|
+
/** Effective permission set for this session. */
|
|
39
|
+
permissions: Permission[];
|
|
40
|
+
/** Opaque session identifier from the host app. */
|
|
41
|
+
sessionId?: string;
|
|
42
|
+
/** Additional host-app claims (non-sensitive). */
|
|
43
|
+
claims?: Record<string, string | number | boolean>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Which SaaS tenant this request belongs to.
|
|
48
|
+
* Must be derived from the authenticated session — never from the LLM.
|
|
49
|
+
*/
|
|
50
|
+
export interface TenantContext {
|
|
51
|
+
/** Tenant id derived from the authenticated session — never from AI params. */
|
|
52
|
+
tenantId: string;
|
|
53
|
+
/** Column used for tenant isolation (default: tenant_id). */
|
|
54
|
+
tenantColumn?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Full per-request context passed into tools and adapters.
|
|
59
|
+
* Combines identity (auth) + tenancy + optional question/correlation ids.
|
|
60
|
+
*/
|
|
61
|
+
export interface RequestContext {
|
|
62
|
+
auth: AuthContext;
|
|
63
|
+
tenant: TenantContext;
|
|
64
|
+
/** Correlation id for audit trails. */
|
|
65
|
+
requestId?: string;
|
|
66
|
+
/** Original natural-language question when available. */
|
|
67
|
+
question?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Normalized column types exposed to AI-facing schema (DB-agnostic). */
|
|
71
|
+
export type ColumnDataType =
|
|
72
|
+
| "string"
|
|
73
|
+
| "number"
|
|
74
|
+
| "boolean"
|
|
75
|
+
| "date"
|
|
76
|
+
| "datetime"
|
|
77
|
+
| "json"
|
|
78
|
+
| "binary"
|
|
79
|
+
| "unknown";
|
|
80
|
+
|
|
81
|
+
/** One column in a table schema returned by an adapter. */
|
|
82
|
+
export interface ColumnSchema {
|
|
83
|
+
name: string;
|
|
84
|
+
dataType: ColumnDataType;
|
|
85
|
+
/** Original DB type name (e.g. "timestamptz", "varchar"). */
|
|
86
|
+
nativeType: string;
|
|
87
|
+
nullable: boolean;
|
|
88
|
+
isPrimaryKey: boolean;
|
|
89
|
+
isForeignKey: boolean;
|
|
90
|
+
/** Present when this column references another table. */
|
|
91
|
+
references?: {
|
|
92
|
+
table: string;
|
|
93
|
+
column: string;
|
|
94
|
+
};
|
|
95
|
+
description?: string;
|
|
96
|
+
/** When true, field is withheld from AI-facing schema/tools. */
|
|
97
|
+
sensitive?: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** One table (or collection) in the inspected database schema. */
|
|
101
|
+
export interface TableSchema {
|
|
102
|
+
name: string;
|
|
103
|
+
/** DB schema/namespace, e.g. "public" in PostgreSQL. */
|
|
104
|
+
schema?: string;
|
|
105
|
+
columns: ColumnSchema[];
|
|
106
|
+
description?: string;
|
|
107
|
+
primaryKey?: string[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Full schema snapshot returned by DatabaseAdapter.getSchema(). */
|
|
111
|
+
export interface DatabaseSchema {
|
|
112
|
+
tables: TableSchema[];
|
|
113
|
+
/** e.g. "postgresql", "mysql". */
|
|
114
|
+
dialect: string;
|
|
115
|
+
/** ISO timestamp of when the schema was inspected. */
|
|
116
|
+
inspectedAt: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Phase 1 only allows read (SELECT) operations by default. */
|
|
120
|
+
export type QueryOperation = "select";
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A query the agent asks an adapter to run.
|
|
124
|
+
* SQL must already be validated as read-only before reaching the adapter.
|
|
125
|
+
*/
|
|
126
|
+
export interface QueryRequest {
|
|
127
|
+
/** Validated SQL (or dialect query) — must be read-only by default. */
|
|
128
|
+
sql: string;
|
|
129
|
+
/** Bound parameters — never interpolate user/AI text into SQL. */
|
|
130
|
+
params?: unknown[];
|
|
131
|
+
/** Max rows returned to the caller. */
|
|
132
|
+
limit?: number;
|
|
133
|
+
/** Query timeout in milliseconds. */
|
|
134
|
+
timeoutMs?: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface QueryResultColumn {
|
|
138
|
+
name: string;
|
|
139
|
+
dataType: ColumnDataType;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Structured result returned to tools / MCP / the LLM. */
|
|
143
|
+
export interface QueryResult {
|
|
144
|
+
columns: QueryResultColumn[];
|
|
145
|
+
rows: Record<string, unknown>[];
|
|
146
|
+
rowCount: number;
|
|
147
|
+
/** True if more rows existed than we returned (limit applied). */
|
|
148
|
+
truncated: boolean;
|
|
149
|
+
executionMs?: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* DatabaseAdapter — pluggable DB driver contract.
|
|
154
|
+
* Postgres/MySQL/MongoDB adapters all implement this so the core stays
|
|
155
|
+
* database-agnostic.
|
|
156
|
+
*/
|
|
157
|
+
export interface DatabaseAdapter {
|
|
158
|
+
readonly name: string;
|
|
159
|
+
getSchema(options?: { includeSensitive?: boolean }): Promise<DatabaseSchema>;
|
|
160
|
+
query(request: QueryRequest, context: RequestContext): Promise<QueryResult>;
|
|
161
|
+
/** Optional cleanup (e.g. close connection pool). */
|
|
162
|
+
close?(): Promise<void>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A named capability the AI can call (built-in or custom).
|
|
167
|
+
* Each tool declares the permission required to run it.
|
|
168
|
+
*/
|
|
169
|
+
export interface ToolDefinition<TInput = unknown, TOutput = unknown> {
|
|
170
|
+
name: string;
|
|
171
|
+
description: string;
|
|
172
|
+
/** Permission required to invoke this tool (e.g. database.query). */
|
|
173
|
+
permission: Permission;
|
|
174
|
+
/** Whether the tool may mutate data. Default false. */
|
|
175
|
+
mutates?: boolean;
|
|
176
|
+
/** Optional JSON-Schema-like description of inputs. */
|
|
177
|
+
inputSchema?: Record<string, unknown>;
|
|
178
|
+
handler: (input: TInput, context: RequestContext) => Promise<TOutput>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* One audit-log entry for an AI tool invocation.
|
|
183
|
+
* Never put passwords, tokens, or connection strings in these fields.
|
|
184
|
+
*/
|
|
185
|
+
export interface AuditEvent {
|
|
186
|
+
timestamp: string;
|
|
187
|
+
requestId?: string;
|
|
188
|
+
userId: string;
|
|
189
|
+
tenantId: string;
|
|
190
|
+
question?: string;
|
|
191
|
+
tool: string;
|
|
192
|
+
parameters?: unknown;
|
|
193
|
+
status: "success" | "denied" | "error";
|
|
194
|
+
errorMessage?: string;
|
|
195
|
+
resultMeta?: {
|
|
196
|
+
rowCount?: number;
|
|
197
|
+
truncated?: boolean;
|
|
198
|
+
executionMs?: number;
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Host-provided callback that receives audit events. */
|
|
203
|
+
export type AuditLogger = (event: AuditEvent) => void | Promise<void>;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Business meaning for a table — helps the AI understand domain language
|
|
207
|
+
* (e.g. "Member" maps to table "members").
|
|
208
|
+
*/
|
|
209
|
+
export interface EntityDefinition {
|
|
210
|
+
name: string;
|
|
211
|
+
table: string;
|
|
212
|
+
description?: string;
|
|
213
|
+
schema?: string;
|
|
214
|
+
relationships?: Array<{
|
|
215
|
+
name: string;
|
|
216
|
+
entity: string;
|
|
217
|
+
type: "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many";
|
|
218
|
+
}>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Marks a column that must never be shown to AI tools. */
|
|
222
|
+
export interface ProtectFieldOptions {
|
|
223
|
+
table: string;
|
|
224
|
+
column: string;
|
|
225
|
+
schema?: string;
|
|
226
|
+
reason?: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Options passed to createMCPDataAgent().
|
|
231
|
+
* This is the main configuration surface for host applications.
|
|
232
|
+
*/
|
|
233
|
+
export interface AgentOptions {
|
|
234
|
+
database: DatabaseAdapter;
|
|
235
|
+
/**
|
|
236
|
+
* Resolve auth + tenant for each request.
|
|
237
|
+
* Tenant MUST come from the authenticated session, never from AI parameters.
|
|
238
|
+
*/
|
|
239
|
+
resolveContext: () => Promise<RequestContext> | RequestContext;
|
|
240
|
+
/** Default max rows for analytical queries. */
|
|
241
|
+
defaultLimit?: number;
|
|
242
|
+
/** Default query timeout in ms. */
|
|
243
|
+
defaultTimeoutMs?: number;
|
|
244
|
+
/** Fields that must never be exposed to AI tools. */
|
|
245
|
+
protectedFields?: ProtectFieldOptions[];
|
|
246
|
+
/** Optional audit sink. Secrets must never be logged. */
|
|
247
|
+
audit?: AuditLogger;
|
|
248
|
+
/** Allow write tools only when explicitly enabled. Default false. */
|
|
249
|
+
allowMutations?: boolean;
|
|
250
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mcp-data-agent/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server bridge for MCP Data Agent. Created by Arslan Habib.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Arslan Habib"
|
|
7
|
+
},
|
|
8
|
+
"creator": "Arslan Habib",
|
|
9
|
+
"license": "UNLICENSED",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json",
|
|
25
|
+
"clean": "rm -rf dist *.tsbuildinfo",
|
|
26
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@mcp-data-agent/core": "0.1.0",
|
|
33
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
34
|
+
"zod": "^3.24.0 || ^4.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@mcp-data-agent/core": "*",
|
|
38
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
39
|
+
"zod": "^4.0.0"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"mcp",
|
|
43
|
+
"model-context-protocol",
|
|
44
|
+
"mcp-data-agent"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Data Agent — @mcp-data-agent/mcp
|
|
3
|
+
* Created by Arslan Habib
|
|
4
|
+
*
|
|
5
|
+
* ---------------------------------------------------------------------------
|
|
6
|
+
* FILE: index.ts
|
|
7
|
+
* PURPOSE:
|
|
8
|
+
* Public entry point for the MCP bridge package.
|
|
9
|
+
*
|
|
10
|
+
* import { createMcpDataAgentServer } from "@mcp-data-agent/mcp";
|
|
11
|
+
* ---------------------------------------------------------------------------
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export { createMcpDataAgentServer } from "./server.js";
|
|
15
|
+
export type { CreateMcpServerOptions } from "./server.js";
|