Skip to content

Commit 316664d

Browse files
adileiadilei
andauthored
Adilei mcp resources update (#408)
* resource links sample * removed gitignore * remove redundant gitignore * removed gitignore * updated serch sever --------- Co-authored-by: adilei <[email protected]>
1 parent 552f381 commit 316664d

32 files changed

+715
-0
lines changed
38.2 KB
Loading
54.4 KB
Loading
529 KB
Loading
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { SpeciesResource } from '../types.js';
2+
export declare const RESOURCES: SpeciesResource[];
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Resource definitions - dynamically generated from species data
2+
import { SPECIES_DATA } from './species.js';
3+
// Dynamically generate resources from species data
4+
export const RESOURCES = SPECIES_DATA.flatMap(species => {
5+
const resources = [
6+
// Text resource for each species
7+
{
8+
uri: `species:///${species.id}/info`,
9+
name: `${species.commonName} - Species Overview`,
10+
description: `Detailed information about the ${species.commonName} including habitat, diet, and conservation status`,
11+
mimeType: "text/plain",
12+
speciesId: species.id,
13+
resourceType: "text"
14+
}
15+
];
16+
// Add image resource only if the species has an image
17+
if (species.image) {
18+
resources.push({
19+
uri: `species:///${species.id}/image`,
20+
name: `${species.commonName} - Photo`,
21+
description: `Photograph of ${species.commonName}`,
22+
mimeType: "image/png",
23+
speciesId: species.id,
24+
resourceType: "image"
25+
});
26+
}
27+
return resources;
28+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { Species } from '../types.js';
2+
export declare const SPECIES_DATA: Species[];
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Static species data
2+
import { encodeImage } from '../utils/imageEncoder.js';
3+
export const SPECIES_DATA = [
4+
{
5+
id: "monarch-butterfly",
6+
commonName: "Monarch Butterfly",
7+
scientificName: "Danaus plexippus",
8+
description: "The Monarch butterfly is famous for its distinctive orange and black wing pattern and its incredible multi-generational migration across North America.",
9+
habitat: "North America, with migration routes between Mexico/California and Canada/United States",
10+
diet: "Larvae feed exclusively on milkweed plants; adults feed on nectar from various flowers",
11+
conservationStatus: "Vulnerable",
12+
interestingFacts: [
13+
"Their migration can span up to 4,800 km (3,000 miles)",
14+
"It takes 3-4 generations to complete the full migration cycle",
15+
"Milkweed makes them toxic to most predators",
16+
"They use the Earth's magnetic field and sun position for navigation"
17+
],
18+
tags: ["insect", "butterfly", "migration", "herbivore", "north-america", "endangered"],
19+
image: encodeImage("butterfly.png")
20+
},
21+
{
22+
id: "red-panda",
23+
commonName: "Red Panda",
24+
scientificName: "Ailurus fulgens",
25+
description: "The red panda is a small arboreal mammal native to the eastern Himalayas. Despite their name, red pandas are not closely related to giant pandas.",
26+
habitat: "Temperate forests in the Himalayas, at elevations between 2,200-4,800 meters",
27+
diet: "Primarily bamboo (95% of diet), but also eggs, birds, insects, and small mammals",
28+
conservationStatus: "Endangered",
29+
interestingFacts: [
30+
"They have a false thumb - an extended wrist bone that helps grip bamboo",
31+
"Red pandas sleep 17 hours a day, often in trees",
32+
"Their thick fur and bushy tail help them stay warm in cold mountain climates",
33+
"They use their tail for balance and as a blanket in cold weather"
34+
],
35+
tags: ["mammal", "herbivore", "endangered", "asia", "himalaya", "arboreal", "cute"],
36+
image: encodeImage("red-panda.png")
37+
},
38+
{
39+
id: "blue-whale",
40+
commonName: "Blue Whale",
41+
scientificName: "Balaenoptera musculus",
42+
description: "The blue whale is the largest animal ever known to have lived on Earth, reaching lengths of up to 30 meters and weighing up to 200 tons.",
43+
habitat: "All major oceans worldwide, migrating between polar feeding grounds and tropical breeding areas",
44+
diet: "Primarily krill (tiny shrimp-like crustaceans), consuming up to 4 tons per day during feeding season",
45+
conservationStatus: "Endangered",
46+
interestingFacts: [
47+
"Their heart can weigh as much as a car and beat only 2 times per minute when diving",
48+
"Their calls can reach 188 decibels, louder than a jet engine",
49+
"A blue whale's tongue can weigh as much as an elephant",
50+
"They can live for 80-90 years in the wild"
51+
],
52+
tags: ["mammal", "marine", "endangered", "carnivore", "ocean", "largest-animal"],
53+
image: encodeImage("blue-whale.png")
54+
},
55+
{
56+
id: "african-elephant",
57+
commonName: "African Elephant",
58+
scientificName: "Loxodonta africana",
59+
description: "The African elephant is the largest land animal on Earth, known for its intelligence, strong social bonds, and distinctive large ears that help regulate body temperature.",
60+
habitat: "Sub-Saharan Africa, including savannas, forests, deserts, and marshes",
61+
diet: "Herbivorous - grasses, leaves, bark, fruits, and roots (up to 300 pounds of vegetation daily)",
62+
conservationStatus: "Vulnerable",
63+
interestingFacts: [
64+
"They can weigh up to 6,000 kg (13,000 pounds) and stand up to 4 meters tall",
65+
"Their trunks contain over 40,000 muscles and can lift up to 350 kg",
66+
"Elephants are highly intelligent with excellent memory and self-awareness",
67+
"They communicate using infrasound frequencies below human hearing range"
68+
],
69+
tags: ["mammal", "herbivore", "africa", "endangered", "largest-land-animal", "intelligent"]
70+
},
71+
{
72+
id: "snow-leopard",
73+
commonName: "Snow Leopard",
74+
scientificName: "Panthera uncia",
75+
description: "The snow leopard is a elusive big cat perfectly adapted to life in the harsh, cold mountains of Central Asia, with its thick fur and powerful build enabling it to hunt in extreme conditions.",
76+
habitat: "Mountain ranges of Central and South Asia, typically at elevations between 3,000-4,500 meters",
77+
diet: "Carnivorous - blue sheep, ibex, marmots, and other mountain mammals",
78+
conservationStatus: "Vulnerable",
79+
interestingFacts: [
80+
"Their thick fur and long tail help them survive temperatures as low as -40°C",
81+
"They can leap up to 15 meters in a single bound",
82+
"Snow leopards cannot roar, unlike other big cats",
83+
"Their wide paws act like natural snowshoes, distributing weight on snow"
84+
],
85+
tags: ["mammal", "carnivore", "asia", "endangered", "mountain", "big-cat", "solitary"]
86+
}
87+
];
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {};
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import express from 'express';
2+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4+
import { z } from 'zod';
5+
import Fuse from 'fuse.js';
6+
import { CallToolRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListResourcesRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema } from "@modelcontextprotocol/sdk/types.js";
7+
import { zodToJsonSchema } from "zod-to-json-schema";
8+
import { SPECIES_DATA } from './data/species.js';
9+
import { RESOURCES } from './data/resources.js';
10+
import { timestamp, formatSpeciesText } from './utils/utils.js';
11+
const app = express();
12+
app.use(express.json());
13+
// Create the MCP server once (reused across requests)
14+
const server = new Server({
15+
name: "biological-species-mcp-server",
16+
version: "1.0.0",
17+
}, {
18+
capabilities: {
19+
resources: { subscribe: true },
20+
tools: {},
21+
},
22+
});
23+
// Tool input schemas
24+
const SearchSpeciesDataSchema = z.object({
25+
searchTerms: z.string().describe("keywords to search for facts about species")
26+
});
27+
const ListSpeciesSchema = z.object({});
28+
// Configure Fuse.js for fuzzy searching
29+
const fuseOptions = {
30+
keys: ['name', 'description'],
31+
threshold: 0.4, // 0 = exact match, 1 = match anything
32+
includeScore: true,
33+
minMatchCharLength: 2
34+
};
35+
const fuse = new Fuse(RESOURCES, fuseOptions);
36+
// Handler: List available tools
37+
server.setRequestHandler(ListToolsRequestSchema, async () => {
38+
return {
39+
tools: [
40+
{
41+
name: "searchSpeciesData",
42+
description: "Search for species resources by title keywords. Returns up to 5 matching resource links (text, images, data packages).",
43+
inputSchema: zodToJsonSchema(SearchSpeciesDataSchema),
44+
},
45+
{
46+
name: "listSpecies",
47+
description: "Get a list of all available species names in the database.",
48+
inputSchema: zodToJsonSchema(ListSpeciesSchema),
49+
},
50+
],
51+
};
52+
});
53+
// Handler: Call tool
54+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
55+
const { name, arguments: args } = request.params;
56+
if (name === "searchSpeciesData") {
57+
const validatedArgs = SearchSpeciesDataSchema.parse(args);
58+
const { searchTerms } = validatedArgs;
59+
console.log(`${timestamp()} 🔍 Client called tool: searchSpeciesData with terms '${searchTerms}'`);
60+
// Use Fuse.js for fuzzy search
61+
const searchResults = fuse.search(searchTerms);
62+
if (searchResults.length === 0) {
63+
console.log(`${timestamp()} ⚠️ No resources found for client search: "${searchTerms}"`);
64+
return {
65+
content: [
66+
{
67+
type: "text",
68+
text: `No resources found matching: "${searchTerms}". Try keywords like 'butterfly', 'panda', 'photo', 'overview', or 'data'.`,
69+
},
70+
],
71+
};
72+
}
73+
// Return top 5 results
74+
const results = searchResults.slice(0, 5).map(result => result.item);
75+
console.log(`${timestamp()} ✅ Returning ${results.length} matching resources to client`);
76+
const content = [
77+
{
78+
type: "text",
79+
text: `Found ${results.length} resource(s) matching "${searchTerms}":\n\n${results.map((r, i) => `${i + 1}. ${r.name}`).join('\n')}`,
80+
},
81+
];
82+
// Add resource references
83+
results.forEach(resource => {
84+
content.push({
85+
type: "resource_link",
86+
uri: resource.uri,
87+
name: resource.name,
88+
description: resource.description,
89+
mimeType: resource.mimeType,
90+
annotations: {
91+
audience: ["assistant"],
92+
priority: 0.8
93+
}
94+
});
95+
});
96+
return { content };
97+
}
98+
if (name === "listSpecies") {
99+
console.log(`${timestamp()} 📋 Client called tool: listSpecies`);
100+
// Get only species names
101+
const speciesNames = SPECIES_DATA.map(species => species.commonName);
102+
console.log(`${timestamp()} ✅ Returning ${speciesNames.length} species names to client`);
103+
return {
104+
content: [
105+
{
106+
type: "text",
107+
text: JSON.stringify(speciesNames, null, 2),
108+
},
109+
],
110+
};
111+
}
112+
throw new Error(`Unknown tool: ${name}`);
113+
});
114+
// Handler: List resources
115+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
116+
console.log(`${timestamp()} 📋 Client requesting list of all ${RESOURCES.length} resources`);
117+
return {
118+
resources: RESOURCES.map(r => ({
119+
uri: r.uri,
120+
name: r.name,
121+
description: r.description,
122+
mimeType: r.mimeType,
123+
}))
124+
};
125+
});
126+
// Handler: Read resource
127+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
128+
const uri = request.params.uri;
129+
console.log(`${timestamp()} 📖 Client reading resource: ${uri}`);
130+
// Find the resource
131+
const resource = RESOURCES.find(r => r.uri === uri);
132+
if (!resource) {
133+
throw new Error(`Unknown resource: ${uri}`);
134+
}
135+
const species = SPECIES_DATA.find(s => s.id === resource.speciesId);
136+
if (!species) {
137+
throw new Error(`Species not found for resource: ${uri}`);
138+
}
139+
console.log(`${timestamp()} 📄 Client requested: ${species.commonName} - ${resource.resourceType}`);
140+
// Return content based on resource type
141+
if (resource.resourceType === 'text') {
142+
const content = formatSpeciesText(species);
143+
console.log(`${timestamp()} 📝 Returning text content to client (${content.length} characters)`);
144+
return {
145+
contents: [
146+
{
147+
uri,
148+
mimeType: "text/plain",
149+
text: content,
150+
},
151+
],
152+
};
153+
}
154+
if (resource.resourceType === 'image') {
155+
console.log(`${timestamp()} 🖼️ Returning image to client for ${species.commonName}`);
156+
return {
157+
contents: [
158+
{
159+
uri,
160+
mimeType: "image/png",
161+
blob: species.image,
162+
},
163+
],
164+
};
165+
}
166+
throw new Error(`Unknown resource type: ${resource.resourceType}`);
167+
});
168+
// Handler: Subscribe to resource updates
169+
server.setRequestHandler(SubscribeRequestSchema, async (request) => {
170+
const { uri } = request.params;
171+
console.log(`${timestamp()} 🔔 Client subscribed to: ${uri}`);
172+
return {};
173+
});
174+
// Handler: Unsubscribe from resource updates
175+
server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
176+
const { uri } = request.params;
177+
console.log(`${timestamp()} 🔕 Client unsubscribed from: ${uri}`);
178+
return {};
179+
});
180+
// Handle MCP requests (stateless mode)
181+
app.post('/mcp', async (req, res) => {
182+
try {
183+
// Create new transport for each request to prevent request ID collisions
184+
const transport = new StreamableHTTPServerTransport({
185+
sessionIdGenerator: undefined,
186+
enableJsonResponse: true
187+
});
188+
res.on('close', () => {
189+
transport.close();
190+
});
191+
await server.connect(transport);
192+
await transport.handleRequest(req, res, req.body);
193+
}
194+
catch (error) {
195+
console.error(`${timestamp()} Error handling MCP request:`, error);
196+
if (!res.headersSent) {
197+
res.status(500).json({
198+
jsonrpc: '2.0',
199+
error: {
200+
code: -32603,
201+
message: 'Internal server error'
202+
},
203+
id: null
204+
});
205+
}
206+
}
207+
});
208+
const PORT = parseInt(process.env.PORT || '3000');
209+
app.listen(PORT, () => {
210+
console.log(`${timestamp()} 🚀 Species MCP Server running on http://localhost:${PORT}/mcp`);
211+
console.log(`${timestamp()} 📚 Loaded ${SPECIES_DATA.length} species and ${RESOURCES.length} resources`);
212+
}).on('error', error => {
213+
console.error(`${timestamp()} Server error:`, error);
214+
process.exit(1);
215+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export interface Species {
2+
id: string;
3+
commonName: string;
4+
scientificName: string;
5+
description: string;
6+
habitat: string;
7+
diet: string;
8+
conservationStatus: string;
9+
interestingFacts: string[];
10+
tags: string[];
11+
image?: string;
12+
}
13+
export interface SpeciesResource {
14+
uri: string;
15+
name: string;
16+
description: string;
17+
mimeType: string;
18+
speciesId: string;
19+
resourceType: 'text' | 'image';
20+
}

0 commit comments

Comments
 (0)