diff --git a/README.md b/README.md index 398d227..66809b4 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,9 @@ An MCP (Model Context Protocol) server that provides comprehensive access to the ## Features -### Core Tools (22 Total) +### Core Tools (24 Total) +- **ChatGPT Compatibility**: `search`, `fetch` - Required tools for ChatGPT MCP connector integration - **Basic Operations**: `getFollowerCount`, `getFollowers`, `getFollowing`, `checkFollowing`, `checkFollower` - **Profile Data**: `fetchAccount`, `fetchProfileStats`, `fetchProfileLists`, `fetchProfileBadges`, `fetchProfileQRCode` - **Advanced Queries**: `fetchProfileFollowing`, `fetchProfileFollowers` with pagination and filtering @@ -17,6 +18,7 @@ An MCP (Model Context Protocol) server that provides comprehensive access to the ### Key Capabilities +- **ChatGPT Compatible**: Implements required `search` and `fetch` tools for ChatGPT MCP connectors - **Tag Filtering**: Filter followers/following by tags (e.g., "top8", "friend", "family") - **ENS Resolution**: Automatic resolution of ENS names to addresses - **Bulk ENS Reverse Resolution**: Convert multiple addresses to ENS names efficiently @@ -26,7 +28,7 @@ An MCP (Model Context Protocol) server that provides comprehensive access to the ## Setup -### For Developers +### For Claude Desktop ```json { @@ -39,6 +41,18 @@ An MCP (Model Context Protocol) server that provides comprehensive access to the } ``` +### For ChatGPT + +1. Go to ChatGPT Settings → Connectors +2. Add Custom Connector with: + - **Name**: ETHID MCP + - **URL**: `https://ethid-mcp.efp.workers.dev/sse` + - **Description**: Search and explore Ethereum Follow Protocol profiles, followers, and social connections +3. Connect and authenticate +4. Use "Search" or "Deep Research" features to access EFP data + +**Note**: ChatGPT requires the `search` and `fetch` tools which are now included in this server. + ## Usage **🚀 IMPORTANT: Before using the ETHID MCP server, run the initialization prompt from [ETHID_MCP_INITIALIZATION_PROMPT.md](./ETHID_MCP_INITIALIZATION_PROMPT.md) to ensure optimal performance and proper tool usage.** @@ -48,6 +62,14 @@ See [USAGE_GUIDE.md](./USAGE_GUIDE.md) for comprehensive examples and best pract ### Quick Examples ```typescript +// ChatGPT-compatible search +await search({ query: 'vitalik.eth' }); +// Result: {"ids": ["profile:vitalik.eth"], "results": [...] } + +// Fetch detailed profile +await fetch({ id: 'profile:vitalik.eth' }); +// Result: Complete profile with followers, following, ENS data + // Get follower count await getFollowerCount({ addressOrName: 'vitalik.eth' }); // Result: "vitalik.eth has 4811 followers and is following 10 accounts." @@ -78,6 +100,7 @@ ethid-mcp/ │ ├── index.ts # Main MCP agent implementation │ ├── tools/ # Modular tool definitions │ │ ├── index.ts # Tool registration coordinator +│ │ ├── chatgpt.ts # ChatGPT compatibility tools (search/fetch) │ │ ├── profile.ts # Profile and following/followers tools │ │ ├── account.ts # Account data and ENS resolution tools │ │ ├── relationships.ts # Relationship checking tools @@ -107,6 +130,7 @@ The ETHID MCP server operates as a Cloudflare Worker that: ### Key Components - **Cloudflare Worker**: Main API integration and business logic +- **ChatGPT Compatibility Layer**: Search/fetch tools for ChatGPT MCP connectors - **Local MCP Server**: Proxy for Claude Desktop integration - **Node.js Wrapper**: Compatibility layer for Node.js v22.12.0+ diff --git a/package.json b/package.json index 3aeb8b1..bd3734d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "wrangler dev", "start": "wrangler dev", "test": "vitest", - "cf-typegen": "wrangler types" + "cf-typegen": "wrangler types", + "typecheck": "tsc --noEmit" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.8.19", diff --git a/src/const/index.ts b/src/const/index.ts new file mode 100644 index 0000000..cebf164 --- /dev/null +++ b/src/const/index.ts @@ -0,0 +1,3 @@ +export const FETCH_LIMIT = 20; + +export const ETH_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/; diff --git a/src/tools/chatgpt.ts b/src/tools/chatgpt.ts new file mode 100644 index 0000000..a773690 --- /dev/null +++ b/src/tools/chatgpt.ts @@ -0,0 +1,225 @@ +import { z } from 'zod'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StatsResponse, AccountResponse, LeaderboardResponse } from '../types/api'; +import { ETH_ADDRESS_REGEX, FETCH_LIMIT } from '../const'; + +// Types for ChatGPT search/fetch pattern +interface SearchResult { + id: string; + title: string; + description: string; + type: 'profile' | 'follower' | 'following' | 'list'; +} + +interface ProfileRecord { + id: string; + address: string; + ens?: string; + followerCount: number; + followingCount: number; + tags?: string[]; + lists?: any[]; + metadata?: any; +} + +export function registerChatGPTTools(server: McpServer, baseUrl: string, ensWorkerUrl: string) { + // Search tool - required by ChatGPT + server.tool( + 'search', + 'Search for Ethereum addresses, ENS names, or profiles in the EFP ecosystem. Returns IDs that can be fetched for detailed information.', + { + query: z.string().describe('Search query - can be an ENS name, Ethereum address, or general search term for profiles'), + limit: z.number().optional().default(10).describe('Maximum number of results to return (default: 10)'), + type: z.enum(['profile', 'follower', 'following', 'all']).optional().default('all').describe('Type of search to perform'), + }, + async ({ query, limit = 10, type = 'all' }) => { + try { + const results: SearchResult[] = []; + + // If query looks like an address or ENS name, search for exact match first + if (query.includes('.eth') || ETH_ADDRESS_REGEX.test(query)) { + try { + const statsResponse = await fetch(`${baseUrl}/users/${encodeURIComponent(query)}/stats`); + if (statsResponse.ok) { + const stats = (await statsResponse.json()) as StatsResponse; + const accountResponse = await fetch(`${baseUrl}/users/${encodeURIComponent(query)}/account`); + const account = accountResponse.ok ? ((await accountResponse.json()) as AccountResponse) : null; + + results.push({ + id: `profile:${query}`, + title: account?.ens?.name || query, + description: `Profile with ${stats.followers_count || 0} followers and following ${stats.following_count || 0} accounts`, + type: 'profile', + }); + } + } catch (e) { + // Profile not found, continue with other searches + console.error('Error fetching profile in search tool:', e); + } + } + + // Search for popular profiles in leaderboard + if ((type === 'follower' || type === 'all') && results.length < limit) { + try { + const lowerQuery = query.toLowerCase(); + const leaderboardResponse = await fetch(`${baseUrl}/discover/leaderboard?limit=${Math.min(FETCH_LIMIT, limit * 2)}`); + if (leaderboardResponse.ok) { + const leaderboard = (await leaderboardResponse.json()) as LeaderboardResponse; + if (leaderboard.results) { + for (const account of leaderboard.results) { + const name = account.ens?.name || account.address; + if (name.toLowerCase().includes(lowerQuery) && results.length < limit) { + results.push({ + id: `profile:${account.address}`, + title: name, + description: `Popular profile with ${account.followers_count || 0} followers`, + type: 'profile', + }); + } + } + } + } + } catch (e) { + console.error('Leaderboard search failed:', e); + } + } + + // If no results found, provide helpful suggestions + if (results.length === 0) { + results.push({ + id: 'help:search', + title: 'Search Help', + description: 'Try searching with an ENS name (e.g., vitalik.eth) or Ethereum address', + type: 'profile', + }); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + ids: results.map((r) => r.id), + results: results, + }), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + ids: [], + error: error instanceof Error ? error.message : 'Unknown error', + }), + }, + ], + }; + } + } + ); + + // Fetch tool - required by ChatGPT + server.tool( + 'fetch', + 'Fetch detailed information for a specific ID returned by the search tool.', + { + id: z.string().describe('ID returned from search results (format: type:identifier)'), + }, + async ({ id }) => { + try { + const [type, identifier] = id.split(':', 2); + + if (type === 'help') { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + title: 'EFP Search Help', + content: 'Search with ENS names like vitalik.eth or Ethereum addresses', + type: 'help', + }), + }, + ], + }; + } + + if (type === 'profile') { + const profileData: ProfileRecord = { + id: id, + address: identifier, + followerCount: 0, + followingCount: 0, + }; + + // Get basic stats + try { + const statsResponse = await fetch(`${baseUrl}/users/${encodeURIComponent(identifier)}/stats`); + if (statsResponse.ok) { + const stats = (await statsResponse.json()) as StatsResponse; + profileData.followerCount = stats.followers_count || 0; + profileData.followingCount = stats.following_count || 0; + } + } catch (e) { + console.error('Error fetching user stats:', e); + } + + // Get account details + try { + const accountResponse = await fetch(`${baseUrl}/users/${encodeURIComponent(identifier)}/account`); + if (accountResponse.ok) { + const account = (await accountResponse.json()) as AccountResponse; + profileData.ens = account.ens?.name; + profileData.metadata = { + avatar: account.ens?.avatar, + description: account.ens?.contenthash, + twitter: account.ens?.records?.['com.twitter'], + github: account.ens?.records?.['com.github'], + discord: account.ens?.records?.['com.discord'], + telegram: account.ens?.records?.['org.telegram'], + website: account.ens?.records?.url, + }; + } + } catch (e) { + console.error('Error fetching account details for profile:', identifier, e); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(profileData, null, 2), + }, + ], + }; + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + error: `Unknown ID format: ${id}`, + }), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + error: error instanceof Error ? error.message : 'Unknown error', + id: id, + }), + }, + ], + }; + } + } + ); +} diff --git a/src/tools/index.ts b/src/tools/index.ts index 011687e..87b7e89 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -8,12 +8,16 @@ import { registerDiscoveryTools } from './discovery'; import { registerListTools } from './lists'; import { registerContextTools } from './context'; import { registerGuidanceTools } from './guidance'; +import { registerChatGPTTools } from './chatgpt'; export function registerAllTools(server: McpServer, env: Env) { const baseUrl = env.EFP_API_URL || 'https://api.ethfollow.xyz/api/v1'; const ensWorkerUrl = env.ENS_WORKER_URL || 'https://ens.ethfollow.xyz'; - // Register all tool categories + // Register ChatGPT compatibility tools first (required: search and fetch) + registerChatGPTTools(server, baseUrl, ensWorkerUrl); + + // Register all other tool categories registerProfileTools(server, baseUrl); registerAccountTools(server, baseUrl, ensWorkerUrl); registerRelationshipTools(server, baseUrl);