Skip to content

Instantly share code, notes, and snippets.

@AvasDream
Last active October 20, 2025 18:27
Show Gist options
  • Select an option

  • Save AvasDream/d61f6a04d69406527eae7a56ab2e8f50 to your computer and use it in GitHub Desktop.

Select an option

Save AvasDream/d61f6a04d69406527eae7a56ab2e8f50 to your computer and use it in GitHub Desktop.
Agent instructions to create mcp server

Agent Instructions: Nutritional Value MCP Server Setup

This guide will help you create a complete MCP (Model Context Protocol) server that provides nutritional information from the USDA FoodData Central API.

IMPORTANT

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.

Project Overview

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/)

Step 1: Create Project Directory Structure

mkdir nutritional-value
cd nutritional-value
mkdir -p ai/api

Step 2: Initialize Node.js Project

Create 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"
  }
}

Step 3: Install Dependencies

npm install

Step 4: Get Your USDA FoodData Central API Key

  1. Visit https://fdc.nal.usda.gov/api-guide.html
  2. Click on "Get an API Key" or go to https://fdc.nal.usda.gov/api-key-signup.html
  3. Fill out the signup form with your email address
  4. You will receive an API key via email (usually instantly)
  5. Copy your API key for the next step

Note: The API is free and doesn't require credit card information.

Step 5: Create Environment Variables File

Create .env file in the project root:

NUTRITION_API_KEY=YOUR_API_KEY_HERE

Replace YOUR_API_KEY_HERE with the API key you received from step 4.

Step 6: Create TypeScript Configuration

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"]
}

Step 7: Create .gitignore File

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/

Step 8: Create Main Server File

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);
  });

Step 9: Create Test Script (Optional but Recommended)

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);
});

Step 10: Running and Testing

Start the Server

npm start

You 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

Run Tests

npm test

This will test searching for "apple" and "banana" and display their nutritional data.

Step 11: Connecting to MCP Client

The server is now running at http://localhost:3000/mcp.

For Local Testing with Claude Desktop or Similar:

Add to your MCP client configuration:

{
  "mcpServers": {
    "nutritional-value": {
      "url": "http://localhost:3000/mcp"
    }
  }
}

For Remote Access (using ngrok):

  1. Install ngrok: https://ngrok.com/download
  2. Run ngrok:
    ngrok http 3000
  3. Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
  4. Use https://abc123.ngrok.io/mcp in your MCP client

Available MCP Tools

1. searchFood

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" }
  }
}

2. getFoodDetails

Get detailed nutritional information for a specific food by its FDC ID.

Input:

{
  "fdcId": 454004
}

Output: Same format as searchFood

Project File Structure

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

Troubleshooting

API Key Issues

  • Verify your .env file contains the correct API key
  • Check that the key doesn't have extra spaces or quotes
  • Make sure .env is in the project root directory

Port Already in Use

PORT=3001 npm start

TypeScript Errors

npm install --save-dev @types/express @types/node

API Rate Limits

The free tier has a limit of 1,000 requests per hour. For production use, contact USDA for higher limits.

Resources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment