This guide will help you create a complete MCP (Model Context Protocol) server that provides nutritional information from the USDA FoodData Central API.
Explain what you are going to implement to the user in a way that he is able to follow you. Adjust your language to the level of technical expertise the user has based on the available conversation history. If no history or memories are available ask the user before starting to explain.
Name: Nutritional Value MCP Server Purpose: Provide nutritional information (calories, protein, carbs, fat, fiber, sugar, sodium) for food items via MCP tools API: USDA FoodData Central API (https://fdc.nal.usda.gov/)
mkdir nutritional-value
cd nutritional-value
mkdir -p ai/apiCreate package.json:
{
"name": "nutritional-value",
"version": "1.0.0",
"description": "MCP server for USDA nutritional data",
"type": "module",
"main": "server.ts",
"scripts": {
"start": "tsx server.ts",
"dev": "tsx watch server.ts",
"test": "tsx test-api.ts",
"test:api": "tsx test-api.ts"
},
"author": "",
"license": "ISC",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.20.1",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/express": "^5.0.3",
"@types/node": "^24.8.1",
"tsx": "^4.19.2"
}
}npm install- Visit https://fdc.nal.usda.gov/api-guide.html
- Click on "Get an API Key" or go to https://fdc.nal.usda.gov/api-key-signup.html
- Fill out the signup form with your email address
- You will receive an API key via email (usually instantly)
- Copy your API key for the next step
Note: The API is free and doesn't require credit card information.
Create .env file in the project root:
NUTRITION_API_KEY=YOUR_API_KEY_HEREReplace YOUR_API_KEY_HERE with the API key you received from step 4.
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"outDir": "./dist",
"rootDir": "."
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "dist"]
}Create .gitignore:
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Build outputs
dist/
build/
*.tsbuildinfo
*.js
*.js.map
*.d.ts
# Environment variables
.env
.env.local
.env.*
*.env
# OS files
.DS_Store
# IDE files
.vscode/
.idea/
Create server.ts:
import {
McpServer,
ResourceTemplate,
} from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import { z } from "zod";
import dotenv from "dotenv";
// Load environment variables
dotenv.config();
// FDC API Configuration
const FDC_API_BASE = "https://api.nal.usda.gov/fdc";
const API_KEY = process.env.NUTRITION_API_KEY;
if (!API_KEY) {
throw new Error("NUTRITION_API_KEY environment variable is required");
}
// Nutrient IDs for common nutrients (these are nutrientId values from the API)
const NUTRIENT_IDS = {
calories: 1008, // Energy
protein: 1003, // Protein
carbs: 1005, // Carbohydrate, by difference
fat: 1004, // Total lipid (fat)
fiber: 1079, // Fiber, total dietary
sugar: 2000, // Total Sugars
sodium: 1093, // Sodium, Na
};
// Types for FDC API responses
interface FoodNutrient {
nutrientId?: number;
nutrientNumber?: string;
nutrientName?: string;
unitName?: string;
value?: number;
amount?: number;
number?: number;
name?: string;
derivationCode?: string;
derivationDescription?: string;
}
interface FoodItem {
fdcId: number;
description: string;
dataType?: string;
foodNutrients: FoodNutrient[];
}
interface SearchResponse {
foods: FoodItem[];
totalHits: number;
}
// Helper function to search for foods by name
async function searchFoodByName(query: string): Promise<FoodItem | null> {
const url = `${FDC_API_BASE}/v1/foods/search?api_key=${API_KEY}&query=${encodeURIComponent(
query
)}&pageSize=1`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`FDC API error: ${response.statusText}`);
}
const data: SearchResponse = await response.json();
return data.foods && data.foods.length > 0 ? data.foods[0] : null;
}
// Helper function to get food details by FDC ID
async function getFoodById(fdcId: number): Promise<FoodItem | null> {
const url = `${FDC_API_BASE}/v1/food/${fdcId}?api_key=${API_KEY}`;
const response = await fetch(url);
if (!response.ok) {
if (response.status === 404) {
return null;
}
throw new Error(`FDC API error: ${response.statusText}`);
}
return await response.json();
}
// Helper function to extract common nutrients from food item
function extractNutrients(food: FoodItem) {
const nutrients: Record<string, { value: number; unit: string }> = {};
for (const [name, id] of Object.entries(NUTRIENT_IDS)) {
// Try different possible structures from the API
const nutrient = food.foodNutrients.find((n) => {
// Check for nutrientId (number) or nutrientNumber (string) or number field
const nutrientId =
n.nutrientId ||
(n.nutrientNumber ? Number(n.nutrientNumber) : undefined) ||
(n.number ? Number(n.number) : undefined);
return nutrientId === id;
});
if (nutrient) {
const value = nutrient.value ?? nutrient.amount ?? 0;
const unit = nutrient.unitName ?? "g";
nutrients[name] = {
value,
unit,
};
}
}
return nutrients;
}
// Create an MCP server
const server = new McpServer({
name: "nutritional-value-server",
version: "1.0.0",
});
// Tool 1: Search for food by name and return nutritional data
server.registerTool(
"searchFood",
{
title: "Search Food",
description:
"Search for a food item by name and get its nutritional information including calories, protein, carbs, fat, fiber, sugar, and sodium",
inputSchema: { query: z.string() },
outputSchema: {
fdcId: z.number(),
name: z.string(),
dataType: z.string().optional(),
nutrients: z.record(
z.object({
value: z.number(),
unit: z.string(),
})
),
},
},
async ({ query }) => {
try {
console.log(`Searching for food: ${query}`);
const food = await searchFoodByName(query);
if (!food) {
return {
content: [
{
type: "text",
text: `No food found matching "${query}". Try a different search term.`,
},
],
};
}
// Debug: log first nutrient structure
if (food.foodNutrients && food.foodNutrients.length > 0) {
console.log("Sample nutrient structure:", JSON.stringify(food.foodNutrients[0], null, 2));
}
const nutrients = extractNutrients(food);
const output = {
fdcId: food.fdcId,
name: food.description,
dataType: food.dataType,
nutrients,
};
console.log(`Found: ${food.description} (FDC ID: ${food.fdcId})`);
return {
content: [
{
type: "text",
text: JSON.stringify(output, null, 2),
},
],
structuredContent: output,
};
} catch (error) {
console.error("Error in searchFood:", error);
return {
content: [
{
type: "text",
text: `Error searching for food: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
}
);
// Tool 2: Get food details by FDC ID
server.registerTool(
"getFoodDetails",
{
title: "Get Food Details",
description:
"Get detailed nutritional information for a specific food using its FDC ID",
inputSchema: { fdcId: z.number() },
outputSchema: {
fdcId: z.number(),
name: z.string(),
dataType: z.string().optional(),
nutrients: z.record(
z.object({
value: z.number(),
unit: z.string(),
})
),
},
},
async ({ fdcId }) => {
try {
console.log(`Getting food details for FDC ID: ${fdcId}`);
const food = await getFoodById(fdcId);
if (!food) {
return {
content: [
{
type: "text",
text: `No food found with FDC ID ${fdcId}.`,
},
],
};
}
const nutrients = extractNutrients(food);
const output = {
fdcId: food.fdcId,
name: food.description,
dataType: food.dataType,
nutrients,
};
console.log(`Found: ${food.description}`);
return {
content: [
{
type: "text",
text: JSON.stringify(output, null, 2),
},
],
structuredContent: output,
};
} catch (error) {
console.error("Error in getFoodDetails:", error);
return {
content: [
{
type: "text",
text: `Error getting food details: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
}
);
// Set up Express and HTTP transport
const app = express();
app.use(express.json());
app.post("/mcp", async (req, res) => {
// Create a new transport for each request to prevent request ID collisions
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true,
});
res.on("close", () => {
transport.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
const port = parseInt(process.env.PORT || "3000");
app
.listen(port, () => {
console.log(
`Nutritional Value MCP Server running on http://localhost:${port}/mcp`
);
console.log("Available tools:");
console.log(" - searchFood: Search for foods by name");
console.log(" - getFoodDetails: Get details for a specific food by FDC ID");
})
.on("error", (error) => {
console.error("Server error:", error);
process.exit(1);
});Create test-api.ts:
import dotenv from "dotenv";
// Load environment variables
dotenv.config();
// FDC API Configuration
const FDC_API_BASE = "https://api.nal.usda.gov/fdc";
const API_KEY = process.env.NUTRITION_API_KEY;
if (!API_KEY) {
throw new Error("NUTRITION_API_KEY environment variable is required");
}
// Nutrient IDs for common nutrients (these are nutrientId values from the API)
const NUTRIENT_IDS = {
calories: 1008, // Energy
protein: 1003, // Protein
carbs: 1005, // Carbohydrate, by difference
fat: 1004, // Total lipid (fat)
fiber: 1079, // Fiber, total dietary
sugar: 2000, // Total Sugars
sodium: 1093, // Sodium, Na
};
// Types for FDC API responses
interface FoodNutrient {
nutrientId?: number;
nutrientNumber?: string;
nutrientName?: string;
unitName?: string;
value?: number;
amount?: number;
number?: number;
name?: string;
}
interface FoodItem {
fdcId: number;
description: string;
dataType?: string;
foodNutrients: FoodNutrient[];
}
interface SearchResponse {
foods: FoodItem[];
totalHits: number;
}
// Helper function to search for foods by name
async function searchFoodByName(query: string): Promise<FoodItem | null> {
const url = `${FDC_API_BASE}/v1/foods/search?api_key=${API_KEY}&query=${encodeURIComponent(
query
)}&pageSize=1`;
console.log(`\nFetching: ${url.replace(API_KEY!, "***")}`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`FDC API error: ${response.statusText}`);
}
const data: SearchResponse = await response.json();
return data.foods && data.foods.length > 0 ? data.foods[0] : null;
}
// Helper function to get food details by FDC ID
async function getFoodById(fdcId: number): Promise<FoodItem | null> {
const url = `${FDC_API_BASE}/v1/food/${fdcId}?api_key=${API_KEY}`;
console.log(`\nFetching: ${url.replace(API_KEY!, "***")}`);
const response = await fetch(url);
if (!response.ok) {
if (response.status === 404) {
return null;
}
throw new Error(`FDC API error: ${response.statusText}`);
}
return await response.json();
}
// Helper function to extract common nutrients from food item
function extractNutrients(food: FoodItem) {
const nutrients: Record<string, { value: number; unit: string }> = {};
console.log(`\n--- Analyzing ${food.foodNutrients.length} nutrients ---`);
// Show first few nutrients for debugging
if (food.foodNutrients && food.foodNutrients.length > 0) {
console.log("\nAll nutrients found:");
food.foodNutrients.forEach((n, idx) => {
console.log(
` [${idx}] ID: ${n.nutrientId}, Number: ${n.nutrientNumber}, Name: ${n.nutrientName}`
);
});
}
for (const [name, id] of Object.entries(NUTRIENT_IDS)) {
// Try different possible structures from the API
const nutrient = food.foodNutrients.find((n) => {
// Check for nutrientId (number) or nutrientNumber (string) or number field
const nutrientId =
n.nutrientId ||
(n.nutrientNumber ? Number(n.nutrientNumber) : undefined) ||
(n.number ? Number(n.number) : undefined);
return nutrientId === id;
});
if (nutrient) {
const value = nutrient.value ?? nutrient.amount ?? 0;
const unit = nutrient.unitName ?? "g";
nutrients[name] = {
value,
unit,
};
console.log(`✓ Found ${name}: ${value} ${unit}`);
} else {
console.log(`✗ Missing ${name} (ID: ${id})`);
}
}
return nutrients;
}
// Test function for a single food
async function testFood(foodName: string) {
console.log("\n" + "=".repeat(60));
console.log(`TESTING: ${foodName.toUpperCase()}`);
console.log("=".repeat(60));
try {
const food = await searchFoodByName(foodName);
if (!food) {
console.log(`❌ No food found matching "${foodName}"`);
return;
}
console.log(`\n✅ Found: ${food.description}`);
console.log(` FDC ID: ${food.fdcId}`);
console.log(` Data Type: ${food.dataType}`);
const nutrients = extractNutrients(food);
console.log("\n--- NUTRITIONAL DATA (per 100g) ---");
Object.entries(nutrients).forEach(([name, data]) => {
console.log(`${name.padEnd(15)}: ${data.value.toFixed(2)} ${data.unit}`);
});
// Test getting by FDC ID
console.log("\n--- TESTING GET BY FDC ID ---");
const foodById = await getFoodById(food.fdcId);
if (foodById) {
console.log(`✅ Successfully retrieved by ID: ${foodById.description}`);
} else {
console.log(`❌ Failed to retrieve by ID: ${food.fdcId}`);
}
} catch (error) {
console.error(`❌ Error testing ${foodName}:`, error);
}
}
// Main test function
async function runTests() {
console.log("Starting FDC API Tests...");
console.log(`API Key present: ${!!API_KEY}`);
// Test apple
await testFood("apple");
// Test banana
await testFood("banana");
console.log("\n" + "=".repeat(60));
console.log("TESTS COMPLETE");
console.log("=".repeat(60));
}
// Run the tests
runTests().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});npm startYou should see:
Nutritional Value MCP Server running on http://localhost:3000/mcp
Available tools:
- searchFood: Search for foods by name
- getFoodDetails: Get details for a specific food by FDC ID
npm testThis will test searching for "apple" and "banana" and display their nutritional data.
The server is now running at http://localhost:3000/mcp.
Add to your MCP client configuration:
{
"mcpServers": {
"nutritional-value": {
"url": "http://localhost:3000/mcp"
}
}
}- Install ngrok: https://ngrok.com/download
- Run ngrok:
ngrok http 3000
- Copy the HTTPS URL (e.g.,
https://abc123.ngrok.io) - Use
https://abc123.ngrok.io/mcpin your MCP client
Search for a food by name and get nutritional information.
Input:
{
"query": "banana"
}Output:
{
"fdcId": 2012128,
"name": "BANANA",
"dataType": "Branded",
"nutrients": {
"calories": { "value": 312, "unit": "KCAL" },
"protein": { "value": 12.5, "unit": "G" },
"carbs": { "value": 40.6, "unit": "G" },
"fat": { "value": 6.25, "unit": "G" },
"fiber": { "value": 6.2, "unit": "G" },
"sugar": { "value": 6.25, "unit": "G" },
"sodium": { "value": 594, "unit": "MG" }
}
}Get detailed nutritional information for a specific food by its FDC ID.
Input:
{
"fdcId": 454004
}Output: Same format as searchFood
nutritional-value/
├── ai/
│ └── api/
│ └── fdc_api.json (optional - API documentation)
├── node_modules/
├── .env (API key - DO NOT COMMIT)
├── .gitignore
├── package.json
├── package-lock.json
├── server.ts (main MCP server)
├── test-api.ts (test script)
├── tsconfig.json
└── README.md
- Verify your
.envfile contains the correct API key - Check that the key doesn't have extra spaces or quotes
- Make sure
.envis in the project root directory
PORT=3001 npm startnpm install --save-dev @types/express @types/nodeThe free tier has a limit of 1,000 requests per hour. For production use, contact USDA for higher limits.
- USDA FoodData Central API Docs: https://fdc.nal.usda.gov/api-guide.html
- MCP SDK Documentation: https://modelcontextprotocol.io/
- Express.js Documentation: https://expressjs.com/