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,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Data Agent — basic integration example
|
|
3
|
+
* Created by Arslan Habib
|
|
4
|
+
*
|
|
5
|
+
* ---------------------------------------------------------------------------
|
|
6
|
+
* FILE: examples/basic/src/index.ts
|
|
7
|
+
* PURPOSE:
|
|
8
|
+
* End-to-end sketch showing how a host app wires:
|
|
9
|
+
* Postgres adapter → createMCPDataAgent → createMcpDataAgentServer → stdio
|
|
10
|
+
*
|
|
11
|
+
* HOW TO RUN (from monorepo root):
|
|
12
|
+
* 1. Copy .env.example → .env and set DATABASE_URL
|
|
13
|
+
* 2. npm run build
|
|
14
|
+
* 3. npm start -w @mcp-data-agent/example-basic
|
|
15
|
+
*
|
|
16
|
+
* Replace resolveContext() with your real session/auth middleware before
|
|
17
|
+
* production use. Never accept tenantId from the LLM.
|
|
18
|
+
* ---------------------------------------------------------------------------
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
createMCPDataAgent,
|
|
23
|
+
Permissions,
|
|
24
|
+
type RequestContext,
|
|
25
|
+
} from "@mcp-data-agent/core";
|
|
26
|
+
import { createMcpDataAgentServer } from "@mcp-data-agent/mcp";
|
|
27
|
+
import { createPostgresAdapter } from "@mcp-data-agent/postgres";
|
|
28
|
+
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* DEMO ONLY — simulates what your auth middleware would return.
|
|
32
|
+
* In production: read the logged-in user + tenant from the session cookie/JWT.
|
|
33
|
+
*/
|
|
34
|
+
async function resolveContext(): Promise<RequestContext> {
|
|
35
|
+
return {
|
|
36
|
+
auth: {
|
|
37
|
+
userId: process.env.DEMO_USER_ID ?? "demo-user",
|
|
38
|
+
displayName: "Demo User",
|
|
39
|
+
roles: ["receptionist"],
|
|
40
|
+
// Receptionist-style permissions (no revenue/expenses)
|
|
41
|
+
permissions: [
|
|
42
|
+
Permissions.DATABASE_SCHEMA,
|
|
43
|
+
Permissions.DATABASE_QUERY,
|
|
44
|
+
"members.read",
|
|
45
|
+
"attendance.read",
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
tenant: {
|
|
49
|
+
// From session — never from tool arguments
|
|
50
|
+
tenantId: process.env.DEMO_TENANT_ID ?? "gym_001",
|
|
51
|
+
},
|
|
52
|
+
question: process.env.DEMO_QUESTION,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function main() {
|
|
57
|
+
// --- 1) Database adapter (prefer read-only DB credentials) ---
|
|
58
|
+
const database = createPostgresAdapter({
|
|
59
|
+
connectionString:
|
|
60
|
+
process.env.DATABASE_URL ?? "postgres://localhost:5432/mcp_data_agent",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// --- 2) Core agent with auth resolution + audit logging ---
|
|
64
|
+
const agent = createMCPDataAgent({
|
|
65
|
+
database,
|
|
66
|
+
resolveContext,
|
|
67
|
+
audit: (event) => {
|
|
68
|
+
// Log to stderr so stdout stays free for MCP stdio protocol
|
|
69
|
+
console.error("[audit]", JSON.stringify(event));
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// --- 3) Business context, field protection, custom domain tool ---
|
|
74
|
+
agent
|
|
75
|
+
.defineEntity({
|
|
76
|
+
name: "Member",
|
|
77
|
+
table: "members",
|
|
78
|
+
description: "A person registered at the gym",
|
|
79
|
+
relationships: [
|
|
80
|
+
{ name: "memberships", entity: "Membership", type: "one-to-many" },
|
|
81
|
+
{ name: "attendance", entity: "Attendance", type: "one-to-many" },
|
|
82
|
+
],
|
|
83
|
+
})
|
|
84
|
+
.protectField({ table: "members", column: "password_hash" })
|
|
85
|
+
.registerTool({
|
|
86
|
+
name: "members.statistics",
|
|
87
|
+
description: "Return basic member statistics for the current tenant",
|
|
88
|
+
permission: "members.read",
|
|
89
|
+
handler: async (_input, context) => {
|
|
90
|
+
// Prefer dedicated domain tools over free-form SQL when possible
|
|
91
|
+
return {
|
|
92
|
+
tenantId: context.tenant.tenantId,
|
|
93
|
+
note: "Wire this to your domain service or a constrained adapter query.",
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// --- 4) Expose tools over MCP stdio for AI assistants ---
|
|
99
|
+
const server = createMcpDataAgentServer({ agent });
|
|
100
|
+
const transport = new StdioServerTransport();
|
|
101
|
+
await server.connect(transport);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
main().catch((error) => {
|
|
105
|
+
console.error(error);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"outDir": "dist",
|
|
6
|
+
"composite": false,
|
|
7
|
+
"noEmit": true
|
|
8
|
+
},
|
|
9
|
+
"include": ["src/**/*.ts"],
|
|
10
|
+
"references": [
|
|
11
|
+
{ "path": "../../packages/core" },
|
|
12
|
+
{ "path": "../../packages/postgres" },
|
|
13
|
+
{ "path": "../../packages/mcp" }
|
|
14
|
+
]
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-data-agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP Data Agent — open-source MCP framework for securely connecting AI assistants to application databases. Created by Arslan Habib.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"ai",
|
|
9
|
+
"database",
|
|
10
|
+
"postgresql",
|
|
11
|
+
"multi-tenant",
|
|
12
|
+
"rbac",
|
|
13
|
+
"llm"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/ArslanKhan99/mcp_engine#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/ArslanKhan99/mcp_engine/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/ArslanKhan99/mcp_engine.git"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Arslan Habib (https://github.com/arslanhabib)",
|
|
25
|
+
"type": "ESM",
|
|
26
|
+
"main": "index.js",
|
|
27
|
+
"directories": {
|
|
28
|
+
"example": "examples"
|
|
29
|
+
},
|
|
30
|
+
"workspaces": [
|
|
31
|
+
"packages/*",
|
|
32
|
+
"examples/*"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "npm run build --workspaces --if-present",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest",
|
|
38
|
+
"typecheck": "tsc -b --pretty",
|
|
39
|
+
"clean": "npm run clean --workspaces --if-present"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"assertion-error": "^2.0.1",
|
|
43
|
+
"cac": "^6.7.14",
|
|
44
|
+
"chai": "^5.3.3",
|
|
45
|
+
"check-error": "^2.1.3",
|
|
46
|
+
"debug": "^4.4.3",
|
|
47
|
+
"deep-eql": "^5.0.2",
|
|
48
|
+
"es-module-lexer": "^1.7.0",
|
|
49
|
+
"esbuild": "^0.25.12",
|
|
50
|
+
"estree-walker": "^3.0.3",
|
|
51
|
+
"expect-type": "^1.4.0",
|
|
52
|
+
"fdir": "^6.5.0",
|
|
53
|
+
"fsevents": "^2.3.3",
|
|
54
|
+
"js-tokens": "^9.0.1",
|
|
55
|
+
"loupe": "^3.2.1",
|
|
56
|
+
"magic-string": "^0.30.21",
|
|
57
|
+
"ms": "^2.1.3",
|
|
58
|
+
"nanoid": "^3.3.19",
|
|
59
|
+
"pathe": "^2.0.3",
|
|
60
|
+
"pathval": "^2.0.1",
|
|
61
|
+
"pg": "^8.23.0",
|
|
62
|
+
"pg-cloudflare": "^1.4.0",
|
|
63
|
+
"pg-connection-string": "^2.14.0",
|
|
64
|
+
"pg-int8": "^1.0.1",
|
|
65
|
+
"pg-pool": "^3.14.0",
|
|
66
|
+
"pg-protocol": "^1.16.0",
|
|
67
|
+
"pg-types": "^2.2.0",
|
|
68
|
+
"pgpass": "^1.0.5",
|
|
69
|
+
"picocolors": "^1.1.1",
|
|
70
|
+
"picomatch": "^4.0.7",
|
|
71
|
+
"postcss": "^8.5.28",
|
|
72
|
+
"postgres-array": "^2.0.0",
|
|
73
|
+
"postgres-bytea": "^1.0.1",
|
|
74
|
+
"postgres-date": "^1.0.7",
|
|
75
|
+
"postgres-interval": "^1.2.0",
|
|
76
|
+
"rollup": "^4.63.3",
|
|
77
|
+
"siginfo": "^2.0.0",
|
|
78
|
+
"source-map-js": "^1.2.1",
|
|
79
|
+
"split2": "^4.2.0",
|
|
80
|
+
"stackback": "^0.0.2",
|
|
81
|
+
"std-env": "^3.10.0",
|
|
82
|
+
"strip-literal": "^3.1.0",
|
|
83
|
+
"tinybench": "^2.9.0",
|
|
84
|
+
"tinyexec": "^0.3.2",
|
|
85
|
+
"tinyglobby": "^0.2.17",
|
|
86
|
+
"tinypool": "^1.1.1",
|
|
87
|
+
"tinyrainbow": "^2.0.0",
|
|
88
|
+
"tinyspy": "^4.0.6",
|
|
89
|
+
"tsx": "^4.23.13",
|
|
90
|
+
"undici-types": "^6.21.0",
|
|
91
|
+
"vite": "^6.4.3",
|
|
92
|
+
"vite-node": "^3.2.4",
|
|
93
|
+
"why-is-node-running": "^2.3.0",
|
|
94
|
+
"xtend": "^4.0.2",
|
|
95
|
+
"zod": "^4.6.5"
|
|
96
|
+
},
|
|
97
|
+
"devDependencies": {
|
|
98
|
+
"@types/node": "^22.13.10",
|
|
99
|
+
"typescript": "^5.8.2",
|
|
100
|
+
"vitest": "^3.0.9"
|
|
101
|
+
},
|
|
102
|
+
"engines": {
|
|
103
|
+
"node": ">=20"
|
|
104
|
+
},
|
|
105
|
+
"creator": "Arslan Habib"
|
|
106
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mcp-data-agent/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Core runtime for MCP Data Agent — auth, RBAC, tenant isolation, schema, and tools. 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
|
+
"keywords": [
|
|
32
|
+
"mcp",
|
|
33
|
+
"mcp-data-agent",
|
|
34
|
+
"database",
|
|
35
|
+
"rbac",
|
|
36
|
+
"multi-tenant"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Data Agent — @mcp-data-agent/core
|
|
3
|
+
* Created by Arslan Habib
|
|
4
|
+
*
|
|
5
|
+
* ---------------------------------------------------------------------------
|
|
6
|
+
* FILE: agent.test.ts
|
|
7
|
+
* PURPOSE:
|
|
8
|
+
* Unit tests for core security behaviors without a real database:
|
|
9
|
+
* - Read-only SQL validation
|
|
10
|
+
* - Sensitive column redaction (schema + query results)
|
|
11
|
+
* - Permission enforcement
|
|
12
|
+
* - Rejection of tenantId overrides from tool params
|
|
13
|
+
* - Custom tools, entities, and audit hooks
|
|
14
|
+
*
|
|
15
|
+
* Uses a mock DatabaseAdapter so tests stay fast and offline.
|
|
16
|
+
* ---------------------------------------------------------------------------
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, expect, it, vi } from "vitest";
|
|
20
|
+
import {
|
|
21
|
+
AuthorizationError,
|
|
22
|
+
createMCPDataAgent,
|
|
23
|
+
QueryValidationError,
|
|
24
|
+
TenantIsolationError,
|
|
25
|
+
validateReadOnlyQuery,
|
|
26
|
+
} from "./index.js";
|
|
27
|
+
import type { DatabaseAdapter, QueryResult, RequestContext } from "./types.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Fake adapter that returns a members table including a sensitive
|
|
31
|
+
* password_hash column — used to assert redaction works.
|
|
32
|
+
*/
|
|
33
|
+
function mockAdapter(overrides?: Partial<DatabaseAdapter>): DatabaseAdapter {
|
|
34
|
+
return {
|
|
35
|
+
name: "mock",
|
|
36
|
+
getSchema: async () => ({
|
|
37
|
+
dialect: "mock",
|
|
38
|
+
inspectedAt: new Date().toISOString(),
|
|
39
|
+
tables: [
|
|
40
|
+
{
|
|
41
|
+
name: "members",
|
|
42
|
+
schema: "public",
|
|
43
|
+
columns: [
|
|
44
|
+
{
|
|
45
|
+
name: "id",
|
|
46
|
+
dataType: "number",
|
|
47
|
+
nativeType: "int",
|
|
48
|
+
nullable: false,
|
|
49
|
+
isPrimaryKey: true,
|
|
50
|
+
isForeignKey: false,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "tenant_id",
|
|
54
|
+
dataType: "string",
|
|
55
|
+
nativeType: "text",
|
|
56
|
+
nullable: false,
|
|
57
|
+
isPrimaryKey: false,
|
|
58
|
+
isForeignKey: false,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "password_hash",
|
|
62
|
+
dataType: "string",
|
|
63
|
+
nativeType: "text",
|
|
64
|
+
nullable: false,
|
|
65
|
+
isPrimaryKey: false,
|
|
66
|
+
isForeignKey: false,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "email",
|
|
70
|
+
dataType: "string",
|
|
71
|
+
nativeType: "text",
|
|
72
|
+
nullable: false,
|
|
73
|
+
isPrimaryKey: false,
|
|
74
|
+
isForeignKey: false,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
}),
|
|
80
|
+
// Deliberately returns password_hash so we can assert it is stripped
|
|
81
|
+
query: async (): Promise<QueryResult> => ({
|
|
82
|
+
columns: [
|
|
83
|
+
{ name: "email", dataType: "string" },
|
|
84
|
+
{ name: "password_hash", dataType: "string" },
|
|
85
|
+
],
|
|
86
|
+
rows: [{ email: "a@example.com", password_hash: "secret" }],
|
|
87
|
+
rowCount: 1,
|
|
88
|
+
truncated: false,
|
|
89
|
+
}),
|
|
90
|
+
...overrides,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Helper: build a RequestContext with the given permissions. */
|
|
95
|
+
function context(perms: string[]): RequestContext {
|
|
96
|
+
return {
|
|
97
|
+
auth: {
|
|
98
|
+
userId: "user-1",
|
|
99
|
+
permissions: perms,
|
|
100
|
+
},
|
|
101
|
+
tenant: { tenantId: "gym_001" },
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
describe("validateReadOnlyQuery", () => {
|
|
106
|
+
it("allows SELECT", () => {
|
|
107
|
+
const result = validateReadOnlyQuery({
|
|
108
|
+
sql: "SELECT id FROM members WHERE active = true",
|
|
109
|
+
});
|
|
110
|
+
expect(result.sql).toContain("SELECT");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("rejects DELETE", () => {
|
|
114
|
+
expect(() =>
|
|
115
|
+
validateReadOnlyQuery({ sql: "DELETE FROM members" }),
|
|
116
|
+
).toThrow(QueryValidationError);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("rejects multi-statement", () => {
|
|
120
|
+
expect(() =>
|
|
121
|
+
validateReadOnlyQuery({
|
|
122
|
+
sql: "SELECT 1; DROP TABLE members",
|
|
123
|
+
}),
|
|
124
|
+
).toThrow(QueryValidationError);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("createMCPDataAgent", () => {
|
|
129
|
+
it("redacts sensitive columns from schema", async () => {
|
|
130
|
+
const agent = createMCPDataAgent({
|
|
131
|
+
database: mockAdapter(),
|
|
132
|
+
resolveContext: () => context(["database.schema"]),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const result = await agent.invokeTool<{
|
|
136
|
+
schema: { tables: Array<{ columns: Array<{ name: string }> }> };
|
|
137
|
+
}>("database.schema");
|
|
138
|
+
|
|
139
|
+
const names = result.schema.tables[0]?.columns.map((c) => c.name) ?? [];
|
|
140
|
+
expect(names).toContain("email");
|
|
141
|
+
expect(names).not.toContain("password_hash");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("redacts sensitive columns from query results", async () => {
|
|
145
|
+
const agent = createMCPDataAgent({
|
|
146
|
+
database: mockAdapter(),
|
|
147
|
+
resolveContext: () => context(["database.query"]),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const result = await agent.invokeTool<{
|
|
151
|
+
rows: Array<Record<string, unknown>>;
|
|
152
|
+
}>("database.query", {
|
|
153
|
+
sql: "SELECT email, password_hash FROM members",
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(result.rows[0]?.email).toBe("a@example.com");
|
|
157
|
+
expect(result.rows[0]?.password_hash).toBeUndefined();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("enforces permissions", async () => {
|
|
161
|
+
// User only has members.read — database.query must be denied
|
|
162
|
+
const agent = createMCPDataAgent({
|
|
163
|
+
database: mockAdapter(),
|
|
164
|
+
resolveContext: () => context(["members.read"]),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await expect(
|
|
168
|
+
agent.invokeTool("database.query", { sql: "SELECT 1" }),
|
|
169
|
+
).rejects.toBeInstanceOf(AuthorizationError);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("rejects tenant overrides from tool params", async () => {
|
|
173
|
+
const agent = createMCPDataAgent({
|
|
174
|
+
database: mockAdapter(),
|
|
175
|
+
resolveContext: () => context(["database.query"]),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await expect(
|
|
179
|
+
agent.invokeTool("database.query", {
|
|
180
|
+
sql: "SELECT 1",
|
|
181
|
+
tenantId: "gym_002", // attack attempt — must throw
|
|
182
|
+
}),
|
|
183
|
+
).rejects.toBeInstanceOf(TenantIsolationError);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("supports custom tools and entities", async () => {
|
|
187
|
+
const audit = vi.fn();
|
|
188
|
+
const agent = createMCPDataAgent({
|
|
189
|
+
database: mockAdapter(),
|
|
190
|
+
resolveContext: () => context(["members.read"]),
|
|
191
|
+
audit,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
agent.defineEntity({
|
|
195
|
+
name: "Member",
|
|
196
|
+
table: "members",
|
|
197
|
+
description: "A gym member",
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
agent.registerTool({
|
|
201
|
+
name: "members.search",
|
|
202
|
+
description: "Search members",
|
|
203
|
+
permission: "members.read",
|
|
204
|
+
handler: async () => ({ items: [] }),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const result = await agent.invokeTool("members.search", { q: "ada" });
|
|
208
|
+
expect(result).toEqual({ items: [] });
|
|
209
|
+
expect(audit).toHaveBeenCalledWith(
|
|
210
|
+
expect.objectContaining({ tool: "members.search", status: "success" }),
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
});
|