-
Notifications
You must be signed in to change notification settings - Fork 18
Added DKG SPARQL query tool and minor code cleanup #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<string, CompleteResourceTemplateCallback> = { | ||
| blockchainName: (val) => | ||
| (Object.values(BLOCKCHAIN_IDS) as string[]).reduce<string[]>( | ||
|
|
@@ -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\`\`\``, | ||
| }, | ||
|
Comment on lines
+199
to
+208
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) }, | ||
| ], | ||
| }; | ||
| }, | ||
| ); | ||
|
|
||
| }); | ||
| 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; | ||
|
|
@@ -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)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}`, | ||
| }; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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