From 12b505725c40da958722684ff7b4db6500b5c83a Mon Sep 17 00:00:00 2001 From: Kevin J <6829515+kmjones1979@users.noreply.github.com> Date: Thu, 5 Jun 2025 22:55:39 -0700 Subject: [PATCH] add subgraph mcp action provider and fix some ssr issues --- agent/the-graph-agent-scaffold-eth/README.md | 2 +- .../packages/nextjs/app/api/chat/route.ts | 7 +- .../packages/nextjs/components/NoSSR.tsx | 26 +++ .../ScaffoldEthAppWithProviders.tsx | 29 +-- .../nextjs/examples/mcp-usage-examples.md | 53 +++++ .../nextjs/services/web3/wagmiConnectors.tsx | 6 +- .../action-providers/graph-mcp-provider.ts | 191 ++++++++++++++++++ .../packages/nextjs/utils/chat/tools.ts | 2 + 8 files changed, 300 insertions(+), 16 deletions(-) create mode 100644 agent/the-graph-agent-scaffold-eth/packages/nextjs/components/NoSSR.tsx create mode 100644 agent/the-graph-agent-scaffold-eth/packages/nextjs/examples/mcp-usage-examples.md create mode 100644 agent/the-graph-agent-scaffold-eth/packages/nextjs/utils/chat/agentkit/action-providers/graph-mcp-provider.ts diff --git a/agent/the-graph-agent-scaffold-eth/README.md b/agent/the-graph-agent-scaffold-eth/README.md index f80128f..6f8be4b 100644 --- a/agent/the-graph-agent-scaffold-eth/README.md +++ b/agent/the-graph-agent-scaffold-eth/README.md @@ -78,7 +78,7 @@ yarn install 3. Set up environment variables in `.env.local`: ```bash -# The Graph Protocol API Key +# The Graph Protocol API Key (used for both regular GraphQL and MCP functionality) GRAPH_API_KEY=your-graph-api-key-here # OpenAI API Key diff --git a/agent/the-graph-agent-scaffold-eth/packages/nextjs/app/api/chat/route.ts b/agent/the-graph-agent-scaffold-eth/packages/nextjs/app/api/chat/route.ts index 829e6ca..681150b 100644 --- a/agent/the-graph-agent-scaffold-eth/packages/nextjs/app/api/chat/route.ts +++ b/agent/the-graph-agent-scaffold-eth/packages/nextjs/app/api/chat/route.ts @@ -39,7 +39,12 @@ export async function POST(req: Request) { You have access to several tools: 1. The chat app has a built-in block explorer so you can link to (for example) /blockexplorer/transaction/ 2. You can query The Graph protocol subgraphs using the querySubgraph action - 3. You can check balances using the contract interactor: + 3. You can use The Graph's MCP (Model Context Protocol) for advanced subgraph discovery and querying: + - searchSubgraphs: Find relevant subgraphs by keyword + - getContractSubgraphs: Find subgraphs that index a specific contract + - getSubgraphSchema: Get the schema for a subgraph to understand available data + - executeMCPQuery: Execute GraphQL queries through MCP + 4. You can check balances using the contract interactor: - For native token balance: Use the "getBalance" action with the user's address - For ERC20 token balances: Use the "getBalance" action with the token contract address and user's address diff --git a/agent/the-graph-agent-scaffold-eth/packages/nextjs/components/NoSSR.tsx b/agent/the-graph-agent-scaffold-eth/packages/nextjs/components/NoSSR.tsx new file mode 100644 index 0000000..5ab1df8 --- /dev/null +++ b/agent/the-graph-agent-scaffold-eth/packages/nextjs/components/NoSSR.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { useEffect, useState } from "react"; + +interface NoSSRProps { + children: React.ReactNode; + fallback?: React.ReactNode; +} + +/** + * NoSSR component that only renders children on the client side + * Prevents hydration mismatches for components that use browser-only APIs + */ +export const NoSSR = ({ children, fallback = null }: NoSSRProps) => { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return <>{fallback}; + } + + return <>{children}; +}; diff --git a/agent/the-graph-agent-scaffold-eth/packages/nextjs/components/ScaffoldEthAppWithProviders.tsx b/agent/the-graph-agent-scaffold-eth/packages/nextjs/components/ScaffoldEthAppWithProviders.tsx index 6853053..2d7fb8a 100644 --- a/agent/the-graph-agent-scaffold-eth/packages/nextjs/components/ScaffoldEthAppWithProviders.tsx +++ b/agent/the-graph-agent-scaffold-eth/packages/nextjs/components/ScaffoldEthAppWithProviders.tsx @@ -9,6 +9,7 @@ import { Toaster } from "react-hot-toast"; import { WagmiProvider } from "wagmi"; import { Footer } from "~~/components/Footer"; import { Header } from "~~/components/Header"; +import { NoSSR } from "~~/components/NoSSR"; import { BlockieAvatar } from "~~/components/scaffold-eth"; import { RainbowKitSiweNextAuthProviderWithSession } from "~~/components/scaffold-eth/RainbowKitSiweNextAuthProviderWithSession"; import { useInitializeNativeCurrencyPrice } from "~~/hooks/scaffold-eth"; @@ -47,18 +48,20 @@ export const ScaffoldEthAppWithProviders = ({ children }: { children: React.Reac }, []); return ( - - - - - - {children} - - - - + Loading...}> + + + + + + {children} + + + + + ); }; diff --git a/agent/the-graph-agent-scaffold-eth/packages/nextjs/examples/mcp-usage-examples.md b/agent/the-graph-agent-scaffold-eth/packages/nextjs/examples/mcp-usage-examples.md new file mode 100644 index 0000000..e6d578d --- /dev/null +++ b/agent/the-graph-agent-scaffold-eth/packages/nextjs/examples/mcp-usage-examples.md @@ -0,0 +1,53 @@ +# The Graph MCP Usage Examples + +This document provides examples of how to interact with The Graph's Model Context Protocol (MCP) through the chat interface. + +## MCP Actions Available + +### 1. Search Subgraphs + +Find relevant subgraphs by keyword + +**Example queries:** + +- "Find subgraphs related to Uniswap" +- "Search for DeFi lending subgraphs" +- "Show me NFT marketplace subgraphs" + +### 2. Get Contract Subgraphs + +Find subgraphs that index a specific contract address + +**Example queries:** + +- "Find subgraphs for contract 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984 on mainnet" (UNI token) +- "What subgraphs index the USDC contract on Ethereum?" +- "Show me subgraphs for Compound's cUSDC contract" + +### 3. Get Subgraph Schema + +Understand what data is available in a subgraph + +**Example queries:** + +- "Show me the schema for subgraph 5zvR82QoaXYFyDEKLZ9t6v9adgnptxYpKpSbxtgVENFV" +- "What entities are available in the Uniswap V3 subgraph?" +- "Get the schema for the Aave subgraph" + +### 4. Execute MCP Query + +Run GraphQL queries through MCP + +**Example queries:** + +- "Query the top 10 Uniswap pools by volume using MCP" +- "Get recent swaps from Uniswap V3 subgraph via MCP" +- "Show me liquidations from Aave using MCP query" + +## Sample Chat Interactions + +### Discovery Workflow + +``` +User: "I want to analyze USDC liquidity across different protocols" +``` diff --git a/agent/the-graph-agent-scaffold-eth/packages/nextjs/services/web3/wagmiConnectors.tsx b/agent/the-graph-agent-scaffold-eth/packages/nextjs/services/web3/wagmiConnectors.tsx index 8167996..cb74fc2 100644 --- a/agent/the-graph-agent-scaffold-eth/packages/nextjs/services/web3/wagmiConnectors.tsx +++ b/agent/the-graph-agent-scaffold-eth/packages/nextjs/services/web3/wagmiConnectors.tsx @@ -13,9 +13,13 @@ import scaffoldConfig from "~~/scaffold.config"; const { onlyLocalBurnerWallet, targetNetworks } = scaffoldConfig; +// Check if we're on the server side +const isServer = typeof window === "undefined"; + const wallets = [ metaMaskWallet, - walletConnectWallet, + // Only include WalletConnect on client side to prevent SSR issues + ...(!isServer ? [walletConnectWallet] : []), ledgerWallet, coinbaseWallet, rainbowWallet, diff --git a/agent/the-graph-agent-scaffold-eth/packages/nextjs/utils/chat/agentkit/action-providers/graph-mcp-provider.ts b/agent/the-graph-agent-scaffold-eth/packages/nextjs/utils/chat/agentkit/action-providers/graph-mcp-provider.ts new file mode 100644 index 0000000..d4a874b --- /dev/null +++ b/agent/the-graph-agent-scaffold-eth/packages/nextjs/utils/chat/agentkit/action-providers/graph-mcp-provider.ts @@ -0,0 +1,191 @@ +import { ActionProvider, WalletProvider } from "@coinbase/agentkit"; +import { z } from "zod"; + +// MCP Client for The Graph +class GraphMCPClient { + private baseUrl: string; + private headers: Record; + + constructor(graphApiKey: string) { + this.baseUrl = "https://subgraphs.mcp.thegraph.com"; + this.headers = { + Authorization: `Bearer ${graphApiKey}`, + "Content-Type": "application/json", + }; + } + + async searchSubgraphs(keyword: string) { + const response = await fetch(`${this.baseUrl}/search`, { + method: "POST", + headers: this.headers, + body: JSON.stringify({ + method: "search_subgraphs_by_keyword", + params: { keyword }, + }), + }); + + if (!response.ok) { + throw new Error(`MCP search failed: ${response.status}`); + } + + return response.json(); + } + + async getTopSubgraphsForContract(contractAddress: string, chain: string) { + const response = await fetch(`${this.baseUrl}/contract-subgraphs`, { + method: "POST", + headers: this.headers, + body: JSON.stringify({ + method: "get_top_subgraph_deployments", + params: { contract_address: contractAddress, chain }, + }), + }); + + if (!response.ok) { + throw new Error(`MCP contract subgraph lookup failed: ${response.status}`); + } + + return response.json(); + } + + async getSubgraphSchema(subgraphId: string) { + const response = await fetch(`${this.baseUrl}/schema`, { + method: "POST", + headers: this.headers, + body: JSON.stringify({ + method: "get_schema_by_subgraph_id", + params: { subgraph_id: subgraphId }, + }), + }); + + if (!response.ok) { + throw new Error(`MCP schema fetch failed: ${response.status}`); + } + + return response.json(); + } + + async executeQuery(subgraphId: string, query: string, variables?: Record) { + const response = await fetch(`${this.baseUrl}/query`, { + method: "POST", + headers: this.headers, + body: JSON.stringify({ + method: "execute_query_by_subgraph_id", + params: { subgraph_id: subgraphId, query, variables }, + }), + }); + + if (!response.ok) { + throw new Error(`MCP query execution failed: ${response.status}`); + } + + return response.json(); + } +} + +// Schema definitions +const searchSubgraphsSchema = z.object({ + keyword: z.string().describe("Keyword to search for in subgraph names and descriptions"), +}); + +const getContractSubgraphsSchema = z.object({ + contractAddress: z.string().describe("The contract address to find subgraphs for"), + chain: z.string().describe("The blockchain network (e.g., 'mainnet', 'polygon', 'arbitrum-one')"), +}); + +const getSchemaSchema = z.object({ + subgraphId: z.string().describe("The subgraph ID to get the schema for"), +}); + +const executeMCPQuerySchema = z.object({ + subgraphId: z.string().describe("The subgraph ID to query"), + query: z.string().describe("The GraphQL query string"), + variables: z.record(z.any()).optional().describe("Optional variables for the GraphQL query"), +}); + +export class GraphMCPProvider implements ActionProvider { + name = "graph-mcp"; + actionProviders = []; + supportsNetwork = () => true; + + private mcpClient: GraphMCPClient; + + constructor() { + const graphApiKey = process.env.GRAPH_API_KEY; + if (!graphApiKey) { + throw new Error("GRAPH_API_KEY not found in environment variables"); + } + this.mcpClient = new GraphMCPClient(graphApiKey); + } + + getActions(walletProvider: WalletProvider) { + return [ + { + name: "searchSubgraphs", + description: "Search for subgraphs by keyword using The Graph's MCP. Returns relevant subgraphs with metadata.", + schema: searchSubgraphsSchema, + severity: "info" as const, + invoke: async ({ keyword }: z.infer) => { + try { + const result = await this.mcpClient.searchSubgraphs(keyword); + return JSON.stringify(result, null, 2); + } catch (error) { + return JSON.stringify({ + error: error instanceof Error ? error.message : "Failed to search subgraphs", + }); + } + }, + }, + { + name: "getContractSubgraphs", + description: "Find the top subgraphs that index a specific contract address on a given blockchain.", + schema: getContractSubgraphsSchema, + severity: "info" as const, + invoke: async ({ contractAddress, chain }: z.infer) => { + try { + const result = await this.mcpClient.getTopSubgraphsForContract(contractAddress, chain); + return JSON.stringify(result, null, 2); + } catch (error) { + return JSON.stringify({ + error: error instanceof Error ? error.message : "Failed to get contract subgraphs", + }); + } + }, + }, + { + name: "getSubgraphSchema", + description: "Get the GraphQL schema for a specific subgraph, showing available entities and fields.", + schema: getSchemaSchema, + severity: "info" as const, + invoke: async ({ subgraphId }: z.infer) => { + try { + const result = await this.mcpClient.getSubgraphSchema(subgraphId); + return JSON.stringify(result, null, 2); + } catch (error) { + return JSON.stringify({ + error: error instanceof Error ? error.message : "Failed to get subgraph schema", + }); + } + }, + }, + { + name: "executeMCPQuery", + description: "Execute a GraphQL query against a subgraph using The Graph's MCP protocol.", + schema: executeMCPQuerySchema, + severity: "info" as const, + invoke: async ({ subgraphId, query, variables = {} }: z.infer) => { + try { + const result = await this.mcpClient.executeQuery(subgraphId, query, variables); + return JSON.stringify(result, null, 2); + } catch (error) { + return JSON.stringify({ + error: error instanceof Error ? error.message : "Failed to execute MCP query", + }); + } + }, + }, + ]; + } +} + +export const graphMCPProvider = () => new GraphMCPProvider(); diff --git a/agent/the-graph-agent-scaffold-eth/packages/nextjs/utils/chat/tools.ts b/agent/the-graph-agent-scaffold-eth/packages/nextjs/utils/chat/tools.ts index cfe0a15..89701ed 100644 --- a/agent/the-graph-agent-scaffold-eth/packages/nextjs/utils/chat/tools.ts +++ b/agent/the-graph-agent-scaffold-eth/packages/nextjs/utils/chat/tools.ts @@ -1,4 +1,5 @@ import { contractInteractor } from "./agentkit/action-providers/contract-interactor"; +import { graphMCPProvider } from "./agentkit/action-providers/graph-mcp-provider"; import { SUBGRAPH_ENDPOINTS, graphQuerierProvider } from "./agentkit/action-providers/graph-querier"; import { tokenApiProvider } from "./agentkit/action-providers/token-api-provider"; import { agentKitToTools } from "./agentkit/framework-extensions/ai-sdk"; @@ -26,6 +27,7 @@ export async function createAgentKit() { walletActionProvider(), contractInteractor(foundry.id), graphQuerierProvider(), + graphMCPProvider(), tokenApiProvider(), ], });