From fcc5ff495ff8609cd84c1f95069086143fcc7dfc Mon Sep 17 00:00:00 2001 From: Jurij Skornik Date: Wed, 10 Dec 2025 11:32:11 +0100 Subject: [PATCH] Added a DKG SPARQL query tool, solved the concurrency issue when running in dev mode, and made some general code cleanup in the plugin-dkg-essentials. --- package.json | 2 +- packages/plugin-dkg-essentials/package.json | 4 +- .../src/plugins/dkg-tools.ts | 105 ++++++++++++++---- packages/plugin-dkg-essentials/src/utils.ts | 43 +++++++ 4 files changed, 131 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 2f6cbbc..dba5f9a 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "build": "turbo run build", "build:check": "turbo format && turbo run lint check-types build", "check": "turbo run check-format check-types lint", - "dev": "turbo run dev --parallel", + "dev": "turbo run dev --parallel --concurrency=100", "lint": "turbo run lint", "format": "prettier --write \"**/*.{ts,tsx,js,jsx,mjs,json,md,yml}\"", "check-format": "prettier --check \"**/*.{ts,tsx,js,jsx,mjs,json,md,yml}\"", diff --git a/packages/plugin-dkg-essentials/package.json b/packages/plugin-dkg-essentials/package.json index 0d1d025..ff16dcb 100644 --- a/packages/plugin-dkg-essentials/package.json +++ b/packages/plugin-dkg-essentials/package.json @@ -29,12 +29,14 @@ "dependencies": { "@dkg/plugin-swagger": "^0.0.2", "@dkg/plugins": "^0.0.2", - "busboy": "^1.6.0" + "busboy": "^1.6.0", + "sparqljs": "^3.7.3" }, "devDependencies": { "@dkg/eslint-config": "*", "@dkg/typescript-config": "*", "@types/busboy": "^1.5.4", + "@types/sparqljs": "^3.1.12", "tsup": "^8.5.0" } } diff --git a/packages/plugin-dkg-essentials/src/plugins/dkg-tools.ts b/packages/plugin-dkg-essentials/src/plugins/dkg-tools.ts index f1aa5a0..85cbd4e 100644 --- a/packages/plugin-dkg-essentials/src/plugins/dkg-tools.ts +++ b/packages/plugin-dkg-essentials/src/plugins/dkg-tools.ts @@ -7,9 +7,10 @@ import { } from "@modelcontextprotocol/sdk/server/mcp.js"; // @ts-expect-error dkg.js import { BLOCKCHAIN_IDS } from "dkg.js/constants"; -import { getExplorerUrl } from "../utils"; +import { getExplorerUrl, validateSparqlQuery } from "../utils"; export default defineDkgPlugin((ctx, mcp) => { + async function publishJsonLdAsset( jsonldRaw: string, privacy: "private" | "public", @@ -30,24 +31,6 @@ export default defineDkgPlugin((ctx, mcp) => { } } - mcp.registerTool( - "dkg-get", - { - title: "DKG Knowledge Asset get tool", - description: - "A tool for running a GET operation on OriginTrail Decentralized Knowledge Graph (DKG) and retrieving a specific Knowledge Asset by its UAL (Unique Asset Locator), taking the UAL as input.", - inputSchema: { ual: z.string() }, - }, - async ({ ual }) => { - const getAssetResult = await ctx.dkg.asset.get(ual); - return { - content: [ - { type: "text", text: JSON.stringify(getAssetResult, null, 2) }, - ], - }; - }, - ); - const ualCompleteOptions: Record = { blockchainName: (val) => (Object.values(BLOCKCHAIN_IDS) as string[]).reduce( @@ -77,8 +60,6 @@ export default defineDkgPlugin((ctx, mcp) => { }, [], ), - // TODO: List possible blockchain contract addresses for v8 and v6 - // blockchainAddress: (val, ctx) =>... }; mcp.registerResource( @@ -179,4 +160,86 @@ export default defineDkgPlugin((ctx, mcp) => { }; }, ); + + mcp.registerTool( + "dkg-sparql-query", + { + title: "DKG SPARQL Query Tool", + description: + "Execute SPARQL queries on the OriginTrail Decentralized Knowledge Graph (DKG). " + + "Takes a SPARQL query as input and returns the query results from the DKG. " + + "Supports SELECT and CONSTRUCT queries.", + inputSchema: { + query: z + .string() + .describe("SPARQL query to execute on the DKG (SELECT or CONSTRUCT)"), + }, + }, + async ({ query }) => { + // Validate query syntax + const validation = validateSparqlQuery(query); + + if (!validation.valid) { + console.error("Invalid SPARQL query:", validation.error); + return { + content: [ + { + type: "text", + text: `❌ Invalid SPARQL query: ${validation.error}\n\nPlease check your query syntax and try again.`, + }, + ], + }; + } + + // Use validated query type + const queryType = validation.queryType || "SELECT"; + + try { + console.log(`Executing SPARQL ${queryType} query...`); + const queryResult = await ctx.dkg.graph.query(query, queryType); + + const resultText = JSON.stringify(queryResult, null, 2); + + return { + content: [ + { + type: "text", + text: `✅ Query executed successfully\n\n**Results:**\n\`\`\`json\n${resultText}\n\`\`\``, + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error("Error executing SPARQL query:", errorMessage); + + return { + content: [ + { + type: "text", + text: `❌ Error executing SPARQL query:\n\n${errorMessage}\n\nPlease check your query and try again.`, + }, + ], + }; + } + }, + ); + + mcp.registerTool( + "dkg-get", + { + title: "DKG Knowledge Asset get tool", + description: + "A tool for running a GET operation on OriginTrail Decentralized Knowledge Graph (DKG) and retrieving a specific Knowledge Asset by its UAL (Unique Asset Locator), taking the UAL as input.", + inputSchema: { ual: z.string() }, + }, + async ({ ual }) => { + const getAssetResult = await ctx.dkg.asset.get(ual); + return { + content: [ + { type: "text", text: JSON.stringify(getAssetResult, null, 2) }, + ], + }; + }, + ); + }); diff --git a/packages/plugin-dkg-essentials/src/utils.ts b/packages/plugin-dkg-essentials/src/utils.ts index 9cb44a3..2c0fa9c 100644 --- a/packages/plugin-dkg-essentials/src/utils.ts +++ b/packages/plugin-dkg-essentials/src/utils.ts @@ -1,4 +1,5 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { Parser } from "sparqljs"; export type SourceKA = { title: string; @@ -62,3 +63,45 @@ export const withSourceKnowledgeAssets = ( ...data, content: [...data.content, serializeSourceKAContent(kas)], }); + +// SPARQL query validation +// Reuse parser instance for better performance +const sparqlParser = new Parser(); + +export function validateSparqlQuery(query: string): { valid: boolean; error?: string; queryType?: string } { + try { + const trimmedQuery = query.trim(); + + if (!trimmedQuery) { + return { valid: false, error: "Query cannot be empty" }; + } + + // Use sparqljs parser for proper syntax validation + const parsed = sparqlParser.parse(trimmedQuery); + + // Check if it's a query (not an update) + if (parsed.type !== "query") { + return { + valid: false, + error: "Only SPARQL queries are allowed, not updates (INSERT, DELETE, MODIFY)", + }; + } + + // Only allow query types supported by the DKG node + const allowedQueryTypes = ["SELECT", "CONSTRUCT"]; + if (!allowedQueryTypes.includes(parsed.queryType)) { + return { + valid: false, + error: `Only SELECT and CONSTRUCT queries are supported. Received: ${parsed.queryType}`, + }; + } + + return { valid: true, queryType: parsed.queryType }; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + return { + valid: false, + error: `Invalid SPARQL syntax: ${errorMessage}`, + }; + } +}