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
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -26,7 +28,7 @@ An MCP (Model Context Protocol) server that provides comprehensive access to the

## Setup

### For Developers
### For Claude Desktop

```json
{
Expand All @@ -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.**
Expand All @@ -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."
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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+

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions src/const/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const FETCH_LIMIT = 20;

export const ETH_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
225 changes: 225 additions & 0 deletions src/tools/chatgpt.ts
Original file line number Diff line number Diff line change
@@ -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,
}),
},
],
};
}
}
);
}
6 changes: 5 additions & 1 deletion src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down