Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}\"",
Expand Down
4 changes: 3 additions & 1 deletion packages/plugin-dkg-essentials/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
105 changes: 84 additions & 21 deletions packages/plugin-dkg-essentials/src/plugins/dkg-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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<string, CompleteResourceTemplateCallback> = {
blockchainName: (val) =>
(Object.values(BLOCKCHAIN_IDS) as string[]).reduce<string[]>(
Expand Down Expand Up @@ -77,8 +60,6 @@ export default defineDkgPlugin((ctx, mcp) => {
},
[],
),
// TODO: List possible blockchain contract addresses for v8 and v6
// blockchainAddress: (val, ctx) =>...
};

mcp.registerResource(
Expand Down Expand Up @@ -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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if query passes validation, queryType should always be set so this fallback might hide potential bugs


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\`\`\``,
},
Comment on lines +199 to +208
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use JSON.stringify when a CONSTRUCT query is called since construct returns a raw string of N-triples?

],
};
} 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) },
],
};
},
);

});
43 changes: 43 additions & 0 deletions packages/plugin-dkg-essentials/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { Parser } from "sparqljs";

export type SourceKA = {
title: string;
Expand Down Expand Up @@ -62,3 +63,45 @@ export const withSourceKnowledgeAssets = <T extends CallToolResult>(
...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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could normalize the case here to make sure queryTyoe is always in uppercase. So something like this:

if (!allowedQueryTypes.includes(parsed.queryType?.toUpperCase()))

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}`,
};
}
}
Loading