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.
Files changed (37) hide show
  1. package/.env.example +18 -0
  2. package/.idea/jsLibraryMappings.xml +6 -0
  3. package/.idea/mcp_engine.iml +8 -0
  4. package/.idea/modules.xml +8 -0
  5. package/.idea/php.xml +19 -0
  6. package/.idea/vcs.xml +6 -0
  7. package/README.md +587 -0
  8. package/examples/basic/package.json +26 -0
  9. package/examples/basic/src/index.ts +107 -0
  10. package/examples/basic/tsconfig.json +15 -0
  11. package/package.json +106 -0
  12. package/packages/core/README.md +7 -0
  13. package/packages/core/package.json +38 -0
  14. package/packages/core/src/agent.test.ts +213 -0
  15. package/packages/core/src/agent.ts +373 -0
  16. package/packages/core/src/errors.ts +73 -0
  17. package/packages/core/src/index.ts +78 -0
  18. package/packages/core/src/permissions.ts +71 -0
  19. package/packages/core/src/query-safety.ts +125 -0
  20. package/packages/core/src/schema-safety.ts +118 -0
  21. package/packages/core/src/tenant.ts +58 -0
  22. package/packages/core/src/tool-registry.ts +61 -0
  23. package/packages/core/src/types.ts +250 -0
  24. package/packages/core/tsconfig.json +10 -0
  25. package/packages/mcp/README.md +7 -0
  26. package/packages/mcp/package.json +46 -0
  27. package/packages/mcp/src/index.ts +15 -0
  28. package/packages/mcp/src/server.ts +170 -0
  29. package/packages/mcp/tsconfig.json +11 -0
  30. package/packages/postgres/README.md +7 -0
  31. package/packages/postgres/package.json +46 -0
  32. package/packages/postgres/src/adapter.ts +358 -0
  33. package/packages/postgres/src/index.ts +19 -0
  34. package/packages/postgres/tsconfig.json +11 -0
  35. package/tsconfig.base.json +22 -0
  36. package/tsconfig.json +8 -0
  37. 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,7 @@
1
+ # @mcp-data-agent/core
2
+
3
+ Core runtime for **MCP Data Agent**.
4
+
5
+ **Created by Arslan Habib**
6
+
7
+ See the root [README](../../README.md) for architecture, security model, and quick start.
@@ -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
+ });