diff --git a/package-lock.json b/package-lock.json index ee3f80c..6598861 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "xp", "version": "0.1.0", "dependencies": { + "@electric-sql/pglite": "^0.2.14", "@hookform/resolvers": "^3.3.4", "@mlc-ai/web-llm": "^0.2.46", "@modelcontextprotocol/sdk": "^1.0.1", @@ -112,6 +113,11 @@ "node": ">=6.9.0" } }, + "node_modules/@electric-sql/pglite": { + "version": "0.2.14", + "resolved": "https://registry.npmmirror.com/@electric-sql/pglite/-/pglite-0.2.14.tgz", + "integrity": "sha512-ZMYZL/yFu5sCewYecdX4OjyOPcrI2OmQ6598e/tyke4Rpgeekd4+pINf9jjzJNJk1Kq5dtuB6buqZsBQf0sx8A==" + }, "node_modules/@emnapi/runtime": { "version": "1.3.1", "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.3.1.tgz", diff --git a/package.json b/package.json index fbf27e7..d788dfe 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@electric-sql/pglite": "^0.2.14", "@hookform/resolvers": "^3.3.4", "@mlc-ai/web-llm": "^0.2.46", "@modelcontextprotocol/sdk": "^1.0.1", diff --git a/src/components/applications/index.tsx b/src/components/applications/index.tsx index 6ef80b4..551a47b 100644 --- a/src/components/applications/index.tsx +++ b/src/components/applications/index.tsx @@ -2,10 +2,12 @@ import React from "react"; import exampleRegister from "@/lib/applications/example"; +import pgliteRegister from "@/lib/applications/pglite"; export default function ApplicationsRegister() { React.useEffect(() => { exampleRegister(); + pgliteRegister(); }, []); return null; } \ No newline at end of file diff --git a/src/lib/applications/pglite/client.ts b/src/lib/applications/pglite/client.ts new file mode 100644 index 0000000..2367a7d --- /dev/null +++ b/src/lib/applications/pglite/client.ts @@ -0,0 +1,18 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; + +export const starter = async (transport: Transport) => { + const client = new Client( + { + name: "pglite-client", + version: "1.0.0", + }, + { + capabilities: {}, + } + ); + + await client.connect(transport); + + return client; +}; diff --git a/src/lib/applications/pglite/index.ts b/src/lib/applications/pglite/index.ts new file mode 100644 index 0000000..e005039 --- /dev/null +++ b/src/lib/applications/pglite/index.ts @@ -0,0 +1,10 @@ +import { registerClient, registerServer } from "../base/host"; +import { starter as clientStarter } from "./client"; +import { starter as serverStarter } from "./server"; + +const register = () => { + registerClient("pglite", clientStarter); + registerServer("pglite", serverStarter); +}; + +export default register; diff --git a/src/lib/applications/pglite/server.ts b/src/lib/applications/pglite/server.ts new file mode 100644 index 0000000..50bea98 --- /dev/null +++ b/src/lib/applications/pglite/server.ts @@ -0,0 +1,113 @@ +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { + CallToolRequestSchema, + ListResourcesRequestSchema, + ListToolsRequestSchema, + ReadResourceRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import { PGlite } from "@electric-sql/pglite"; + +const mainDb = new PGlite("idb://xp-main-pgdata"); + +export const starter = async (transport: Transport) => { + const server = new Server( + { + name: "pglite-server", + version: "1.0.0", + }, + { + capabilities: { + resources: {}, + tools: {}, + }, + } + ); + + const SCHEMA_PATH = "schema"; + + server.setRequestHandler(ListResourcesRequestSchema, async () => { + const result = await mainDb.query( + "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'" + ); + return { + resources: (result.rows as any[]).map((row) => ({ + uri: new URL(`${row.table_name}/${SCHEMA_PATH}`).href, + mimeType: "application/json", + name: `"${row.table_name}" database schema`, + })), + }; + }); + + server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const resourceUrl = new URL(request.params.uri); + + const pathComponents = resourceUrl.pathname.split("/"); + const schema = pathComponents.pop(); + const tableName = pathComponents.pop(); + + if (schema !== SCHEMA_PATH) { + throw new Error("Invalid resource URI"); + } + + const result = await mainDb.query( + "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1", + [tableName] + ); + + return { + contents: [ + { + uri: request.params.uri, + mimeType: "application/json", + text: JSON.stringify(result.rows, null, 2), + }, + ], + }; + }); + + server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "query", + description: "Run a read-only SQL query", + inputSchema: { + type: "object", + properties: { + sql: { type: "string" }, + }, + }, + }, + ], + }; + }); + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (request.params.name === "query") { + const sql = request.params.arguments?.sql as string; + + try { + await mainDb.query("BEGIN TRANSACTION READ ONLY"); + const result = await mainDb.query(sql); + return { + content: [ + { type: "text", text: JSON.stringify(result.rows, null, 2) }, + ], + isError: false, + }; + } catch (error) { + throw error; + } finally { + mainDb + .query("ROLLBACK") + .catch((error) => + console.warn("Could not roll back transaction:", error) + ); + } + } + throw new Error(`Unknown tool: ${request.params.name}`); + }); + + await server.connect(transport); +};