> ## Documentation Index
> Fetch the complete documentation index at: https://aitutorial.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Model Context Protocol (MCP)

> Tools, MCP servers, and how agents interact with external systems

export const QuizQuestion = ({question, options, answer, explanation}) => {
  const [selected, setSelected] = useState(null);
  const [revealed, setRevealed] = useState(false);
  const handleSelect = index => {
    if (revealed) return;
    setSelected(index);
    setRevealed(true);
  };
  const isCorrect = selected === answer;
  const getOptionClass = i => {
    const classes = ['quiz-option'];
    if (revealed) {
      classes.push('quiz-option-disabled');
      if (i === answer) classes.push('quiz-option-correct'); else if (i === selected && !isCorrect) classes.push('quiz-option-wrong');
    }
    return classes.join(' ');
  };
  return <div className="quiz-card">
      <p className="quiz-question">{question}</p>
      <div className="quiz-options">
        {options.map((option, i) => <button key={i} onClick={() => handleSelect(i)} className={getOptionClass(i)}>
            <span className="quiz-letter">{String.fromCharCode(65 + i)}</span>
            {option}
          </button>)}
      </div>
      {revealed && <div className={`quiz-feedback ${isCorrect ? 'quiz-feedback-correct' : 'quiz-feedback-wrong'}`}>
          <strong>{isCorrect ? 'Correct!' : 'Incorrect.'}</strong> {explanation}
        </div>}
    </div>;
};

export const Quiz = ({title = "Check Your Understanding", children}) => {
  return <div style={{
    marginTop: '24px'
  }}>
      <div className="quiz-title">{title}</div>
      {children}
    </div>;
};

export const CodeEditor = ({file = 'src/hello_world.ts', lines, title = 'Code Example', repo = 'ai-tutorial/typescript-examples', height = '650px', functionName, theme: userTheme}) => {
  const STORAGE_KEY = 'openai_api_key';
  const GEMINI_STORAGE_KEY = 'gemini_api_key';
  const ANTHROPIC_STORAGE_KEY = 'anthropic_api_key';
  const PROVIDER_STORAGE_KEY = 'llm_playground_provider';
  if (!functionName) {
    console.warn('CodeEditor: functionName parameter is required');
  }
  const hasCreatedEnvRef = useRef(false);
  const vmRef = useRef(null);
  const [isMaximized, setIsMaximized] = useState(false);
  const [isCollapsed, setIsCollapsed] = useState(false);
  const [isStuck, setIsStuck] = useState(false);
  const [iframeKey, setIframeKey] = useState(0);
  const [showApiKeyDialog, setShowApiKeyDialog] = useState(false);
  const [apiKey, setApiKey] = useState('');
  const [error, setError] = useState('');
  const [success, setSuccess] = useState(false);
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [isValidating, setIsValidating] = useState(false);
  const [detectedTheme, setDetectedTheme] = useState('dark');
  useEffect(() => {
    if (typeof window === 'undefined') return;
    const checkTheme = () => {
      const isDark = document.documentElement.classList.contains('dark');
      setDetectedTheme(isDark ? 'dark' : 'light');
    };
    checkTheme();
    const observer = new MutationObserver(checkTheme);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['class']
    });
    return () => observer.disconnect();
  }, []);
  const theme = userTheme || detectedTheme;
  const [selectedProvider, setSelectedProvider] = useState(() => {
    if (typeof window === 'undefined') return 'gemini';
    return localStorage.getItem(PROVIDER_STORAGE_KEY) || 'gemini';
  });
  const isApiKeyConfigured = () => {
    const openaiKey = localStorage.getItem(STORAGE_KEY);
    const geminiKey = localStorage.getItem(GEMINI_STORAGE_KEY);
    const anthropicKey = localStorage.getItem(ANTHROPIC_STORAGE_KEY);
    return openaiKey !== null && openaiKey.trim().length > 0 || geminiKey !== null && geminiKey.trim().length > 0 || anthropicKey !== null && anthropicKey.trim().length > 0;
  };
  const dispatchApiKeyChanged = () => {
    if (typeof window !== 'undefined' && window.dispatchEvent) {
      window.dispatchEvent(new CustomEvent('apiKeyChanged', {
        detail: {
          configured: isApiKeyConfigured()
        }
      }));
    }
  };
  const saveApiKey = apiKey => {
    if (apiKey && apiKey.trim()) {
      const trimmedKey = apiKey.trim();
      localStorage.setItem(STORAGE_KEY, trimmedKey);
      dispatchApiKeyChanged();
      return true;
    }
    return false;
  };
  const buildEnvContent = () => {
    const openaiKey = localStorage.getItem(STORAGE_KEY)?.trim();
    const geminiKey = localStorage.getItem(GEMINI_STORAGE_KEY)?.trim();
    const anthropicKey = localStorage.getItem(ANTHROPIC_STORAGE_KEY)?.trim();
    if (!openaiKey && !geminiKey && !anthropicKey) {
      return `OPENAI_MODEL=gpt-4.1-nano
OPENAI_API_KEY=sk-mock-key-1234567890abcdef
GEMINI_MODEL=gemini-2.5-flash-lite
GOOGLE_GENERATIVE_AI_API_KEY=
GOOGLE_API_KEY=
ANTHROPIC_API_KEY=
AI_PROVIDER=openai
# API key not found in browser storage
# To configure your API key:
# 1. For Gemini (free): Go to https://aistudio.google.com/apikey
# 2. For OpenAI: Go to https://platform.openai.com/api-keys
# 3. For Claude: Go to https://console.anthropic.com/settings/keys
# 4. Enter it in the configuration form above this editor
# 5. The .env file will be automatically updated with your key`;
    }
    const envLines = ['# Using the API key(s) you configured. This file will be created when the dialog is loaded.'];
    if (openaiKey) {
      envLines.push(`OPENAI_MODEL=gpt-4.1-nano`);
      envLines.push(`OPENAI_API_KEY=${openaiKey}`);
    }
    if (geminiKey) {
      envLines.push(`GEMINI_MODEL=gemini-2.5-flash-lite`);
      envLines.push(`# Vercel AI SDK uses GOOGLE_GENERATIVE_AI_API_KEY, LangChain uses GOOGLE_API_KEY`);
      envLines.push(`GOOGLE_GENERATIVE_AI_API_KEY=${geminiKey}`);
      envLines.push(`GOOGLE_API_KEY=${geminiKey}`);
    }
    if (anthropicKey) {
      envLines.push(`ANTHROPIC_API_KEY=${anthropicKey}`);
    }
    const provider = anthropicKey ? 'anthropic' : geminiKey ? 'gemini' : 'openai';
    envLines.push(`AI_PROVIDER=${provider}`);
    return envLines.join('\n');
  };
  const updateEnvFile = async vm => {
    if (!vm) return;
    try {
      await vm.applyFsDiff({
        create: {
          'env/.env': buildEnvContent(),
          'env/run.conf': `file=${file}`
        },
        destroy: []
      });
      hasCreatedEnvRef.current = true;
    } catch (error) {
      console.error('Failed to write env files:', error);
      hasCreatedEnvRef.current = false;
    }
  };
  useEffect(() => {
    if (!isApiKeyConfigured()) {
      setShowApiKeyDialog(true);
    }
    const handleApiKeyChanged = () => {
      if (isApiKeyConfigured()) {
        setShowApiKeyDialog(false);
      }
    };
    if (typeof window !== 'undefined') {
      window.addEventListener('apiKeyChanged', handleApiKeyChanged);
      return () => {
        window.removeEventListener('apiKeyChanged', handleApiKeyChanged);
      };
    }
  }, []);
  const validateApiKey = async (key, provider) => {
    try {
      const urls = {
        gemini: 'https://generativelanguage.googleapis.com/v1beta/models?key=' + encodeURIComponent(key.trim()),
        openai: 'https://api.openai.com/v1/models',
        anthropic: 'https://api.anthropic.com/v1/models'
      };
      const headerMap = {
        gemini: {
          'Content-Type': 'application/json'
        },
        openai: {
          'Authorization': `Bearer ${key.trim()}`,
          'Content-Type': 'application/json'
        },
        anthropic: {
          'x-api-key': key.trim(),
          'anthropic-version': '2023-06-01',
          'Content-Type': 'application/json'
        }
      };
      const url = urls[provider];
      const headers = headerMap[provider];
      const response = await fetch(url, {
        method: 'GET',
        headers
      });
      if (response.ok) {
        return {
          valid: true
        };
      } else if (response.status === 401 || response.status === 403) {
        return {
          valid: false,
          error: 'Invalid API key. Please check your key and try again.'
        };
      } else if (response.status === 429) {
        return {
          valid: false,
          error: 'Rate limit exceeded. Please try again later.'
        };
      } else {
        const errorData = await response.json().catch(() => ({}));
        return {
          valid: false,
          error: errorData.error?.message || `API request failed with status ${response.status}`
        };
      }
    } catch (err) {
      if (err.name === 'TypeError' && err.message.includes('fetch')) {
        return {
          valid: false,
          error: 'Network error. Please check your connection and try again.'
        };
      }
      return {
        valid: false,
        error: err.message || 'Failed to validate API key. Please try again.'
      };
    }
  };
  const handleSkipConfiguration = () => {
    const skipKey = 'sk-<configure-your-key>';
    saveApiKey(skipKey);
    setShowApiKeyDialog(false);
  };
  const handleApiKeySubmit = async e => {
    e.preventDefault();
    setError('');
    setSuccess(false);
    setIsSubmitting(true);
    const providerNames = {
      gemini: 'Gemini',
      openai: 'OpenAI',
      anthropic: 'Claude'
    };
    if (!apiKey || !apiKey.trim()) {
      setError(`Please enter your ${providerNames[selectedProvider]} API key`);
      setIsSubmitting(false);
      return;
    }
    const trimmedKey = apiKey.trim();
    if (selectedProvider === 'openai' && !trimmedKey.startsWith('sk-')) {
      setError('Invalid API key format. OpenAI API keys should start with "sk-"');
      setIsSubmitting(false);
      return;
    }
    if (selectedProvider === 'anthropic' && !trimmedKey.startsWith('sk-ant-')) {
      setError('Invalid API key format. Anthropic API keys should start with "sk-ant-"');
      setIsSubmitting(false);
      return;
    }
    setIsValidating(true);
    setError('');
    const validation = await validateApiKey(trimmedKey, selectedProvider);
    setIsValidating(false);
    if (!validation.valid) {
      setError(validation.error || 'Invalid API key. Please check your key and try again.');
      setIsSubmitting(false);
      return;
    }
    try {
      const storageKeys = {
        gemini: GEMINI_STORAGE_KEY,
        openai: STORAGE_KEY,
        anthropic: ANTHROPIC_STORAGE_KEY
      };
      localStorage.setItem(storageKeys[selectedProvider], trimmedKey);
      localStorage.setItem(PROVIDER_STORAGE_KEY, selectedProvider);
      dispatchApiKeyChanged();
      setSuccess(true);
      setApiKey('');
      setTimeout(() => {
        window.location.reload();
      }, 1000);
    } catch (err) {
      setError(err.message || 'Failed to save API key. Please try again.');
      setIsSubmitting(false);
    }
  };
  const baseFilePath = file || 'src/hello_world.ts';
  let filePath = baseFilePath;
  if (typeof lines === 'string' && lines.trim()) {
    const lineParts = lines.split('-');
    if (lineParts.length === 2) {
      filePath = `${filePath}:L${lineParts[0].trim()}-L${lineParts[1].trim()}`;
    } else {
      filePath = `${filePath}:L${lineParts[0].trim()}`;
    }
  } else if (typeof lines === 'object' && lines.start !== undefined) {
    filePath = lines.end !== undefined ? `${filePath}:L${lines.start}-L${lines.end}` : `${filePath}:L${lines.start}`;
  }
  const stackblitzUrl = `https://stackblitz.com/github/${repo}?file=${encodeURIComponent(filePath)}&embed=1&view=editor&theme=${theme}`;
  const loadSDK = () => {
    return new Promise((resolve, reject) => {
      if (window.StackBlitzSDK || window.stackblitzSDK) {
        resolve(window.StackBlitzSDK || window.stackblitzSDK);
        return;
      }
      if (document.querySelector('script[data-stackblitz-sdk]')) {
        const checkInterval = setInterval(() => {
          if (window.StackBlitzSDK || window.stackblitzSDK) {
            clearInterval(checkInterval);
            resolve(window.StackBlitzSDK || window.stackblitzSDK);
          }
        }, 100);
        setTimeout(() => {
          clearInterval(checkInterval);
          reject(new Error('SDK loading timeout'));
        }, 10000);
        return;
      }
      const script = document.createElement('script');
      script.src = 'https://unpkg.com/@stackblitz/sdk/bundles/sdk.umd.js';
      script.async = true;
      script.setAttribute('data-stackblitz-sdk', 'true');
      script.onload = () => {
        const sdk = window.StackBlitzSDK || window.stackblitzSDK;
        if (sdk) {
          resolve(sdk);
        } else {
          reject(new Error('SDK loaded but not available on window'));
        }
      };
      script.onerror = () => {
        reject(new Error('Failed to load StackBlitz SDK'));
      };
      document.head.appendChild(script);
    });
  };
  const LOAD_TIMEOUT_MS = 10000;
  const iframeElRef = useRef(null);
  const reloadCountRef = useRef(0);
  const handleRetry = () => {
    vmRef.current = null;
    hasCreatedEnvRef.current = false;
    reloadCountRef.current = 0;
    setIsStuck(false);
    setIframeKey(prev => prev + 1);
  };
  const iframeRef = iframe => {
    iframeElRef.current = iframe;
  };
  const connectToVM = async iframe => {
    const sdk = await loadSDK();
    return sdk.connect(iframe);
  };
  const handleIframeLoad = async () => {
    const iframe = iframeElRef.current;
    if (!iframe) return;
    if (reloadCountRef.current > 0) {
      try {
        const vm = await connectToVM(iframe);
        vmRef.current = vm;
        await updateEnvFile(vm);
      } catch (_) {}
      return;
    }
    try {
      if (vmRef.current) return;
      const vm = await Promise.race([connectToVM(iframe), new Promise((_, reject) => setTimeout(() => reject(new Error('connect timeout')), LOAD_TIMEOUT_MS))]);
      vmRef.current = vm;
      await updateEnvFile(vm);
    } catch (error) {
      console.error('Failed to connect to StackBlitz VM:', error);
      if (typeof window !== 'undefined' && window.gtag) {
        window.gtag('event', 'load_refresh_error', {
          event_category: 'stackblitz',
          event_label: file,
          error_message: error.message
        });
      }
      reloadCountRef.current = 1;
      setTimeout(() => {
        setIframeKey(prev => prev + 1);
      }, 2000);
    }
  };
  const isSafari = typeof navigator !== 'undefined' && (/^((?!chrome|android).)*safari/i).test(navigator.userAgent);
  if (isSafari) {
    return <div className="code-editor-dialog-container" style={{
      height: height
    }}>
        <div className="code-editor-dialog-box">
          <h2 className="code-editor-dialog-title">
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" strokeWidth="2">
              <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
              <line x1="12" y1="9" x2="12" y2="13"></line>
              <line x1="12" y1="17" x2="12.01" y2="17"></line>
            </svg>
            Browser Not Supported
          </h2>
          <p className="code-editor-dialog-description">
            The interactive code editor is not supported on Safari. Please use <strong>Chrome</strong>, <strong>Edge</strong>, or <strong>Firefox</strong> to run the examples.
          </p>
        </div>
      </div>;
  }
  if (showApiKeyDialog) {
    return <div className="code-editor-dialog-container" style={{
      height: height
    }}>
        <div className="code-editor-dialog-box">
          <h2 className="code-editor-dialog-title">
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" strokeWidth="2">
              <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
              <line x1="12" y1="9" x2="12" y2="13"></line>
              <line x1="12" y1="17" x2="12.01" y2="17"></line>
            </svg>
            Configure API Key
          </h2>

          <p className="code-editor-dialog-description">
            All interactive examples execute entirely within your browser environment, ensuring complete security and privacy.
            Your API key is stored locally in your browser's storage and is never transmitted to external servers.
          </p>

          <div className="llm-provider-tabs" style={{
      marginBottom: '16px'
    }}>
            <button type="button" onClick={() => {
      setSelectedProvider('gemini');
      setError('');
      setApiKey('');
    }} className={`llm-provider-tab ${selectedProvider === 'gemini' ? 'llm-provider-tab-active' : ''}`}>
              Gemini <span className="llm-provider-tab-badge">Free</span>
            </button>
            <button type="button" onClick={() => {
      setSelectedProvider('openai');
      setError('');
      setApiKey('');
    }} className={`llm-provider-tab ${selectedProvider === 'openai' ? 'llm-provider-tab-active' : ''}`}>
              OpenAI
            </button>
            <button type="button" onClick={() => {
      setSelectedProvider('anthropic');
      setError('');
      setApiKey('');
    }} className={`llm-provider-tab ${selectedProvider === 'anthropic' ? 'llm-provider-tab-active' : ''}`}>
              Claude
            </button>
          </div>

          {selectedProvider === 'gemini' && <div className="llm-gemini-recommendation" style={{
      marginBottom: '16px'
    }}>
              Gemini offers a generous free tier — great for learning! Get your free API key at{' '}
              <a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer" className="code-editor-link">
                aistudio.google.com/apikey
              </a>
            </div>}

          {selectedProvider === 'openai' && <div className="code-editor-info-box">
              <p className="code-editor-info-box-title">
                Don't have an API key?
              </p>
              <p className="code-editor-info-box-text">
                Get one at{' '}
                <a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer" className="code-editor-link">
                  platform.openai.com/api-keys
                </a>
              </p>
            </div>}

          {selectedProvider === 'anthropic' && <div className="code-editor-info-box">
              <p className="code-editor-info-box-title">
                Don't have an API key?
              </p>
              <p className="code-editor-info-box-text">
                Get one at{' '}
                <a href="https://console.anthropic.com/settings/keys" target="_blank" rel="noopener noreferrer" className="code-editor-link">
                  console.anthropic.com/settings/keys
                </a>
              </p>
            </div>}

          <form onSubmit={handleApiKeySubmit}>
            <div className="code-editor-form-group">
              <label htmlFor="api-key-input" className="code-editor-label">
                {selectedProvider === 'gemini' ? 'Gemini' : 'OpenAI'} API Key
              </label>
              <input id="api-key-input" type="password" value={apiKey} onChange={e => {
      setApiKey(e.target.value);
      setError('');
      setSuccess(false);
    }} placeholder={selectedProvider === 'openai' ? 'sk-...' : 'Gemini API Key'} disabled={isSubmitting} className={`code-editor-input ${error ? 'code-editor-input-error' : ''}`} />
            </div>

            {isValidating && <div className="code-editor-message code-editor-message-info">
                <span className="code-editor-message-icon">⏳</span>
                <span>Validating API key...</span>
              </div>}

            {error && !isValidating && <div className="code-editor-message code-editor-message-error">
                <span className="code-editor-message-icon">⚠️</span>
                <span>{error}</span>
              </div>}

            {success && <div className="code-editor-message code-editor-message-success">
                <span className="code-editor-message-icon">✓</span>
                <span>API key saved successfully! Loading editor...</span>
              </div>}

            <button type="submit" disabled={isSubmitting || isValidating || !apiKey.trim()} className="code-editor-button">
              {isValidating ? 'Validating...' : isSubmitting ? 'Saving...' : 'Save API Key'}
            </button>
          </form>

          <button type="button" onClick={handleSkipConfiguration} disabled={isSubmitting || isValidating} className="code-editor-button-secondary">
            Skip Configuration
          </button>

          <div className="code-editor-footer">
            <p className="code-editor-footer-text">
              Alternatively, you may checkout the source code from{' '}
              <a href="https://github.com/ai-tutorial/typescript-examples" target="_blank" rel="noopener noreferrer" className="code-editor-link code-editor-link-break">
                https://github.com/ai-tutorial/typescript-examples
              </a>
              {' '}and run the examples locally.
            </p>
          </div>
        </div>
      </div>;
  }
  const toggleMaximize = () => setIsMaximized(!isMaximized);
  const toggleCollapse = () => setIsCollapsed(!isCollapsed);
  return <div className={`code-editor-wrapper ${isMaximized ? 'maximized' : ''} ${isCollapsed ? 'collapsed' : ''}`} data-theme={theme}>
      <div className="code-editor-header">
        <div className="code-editor-title">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
            <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
          </svg>
          {title}
        </div>
        <div className="code-editor-controls">
          {!isMaximized && <button className="code-editor-collapse-button" onClick={toggleCollapse} title={isCollapsed ? "Expand" : "Collapse"} type="button">
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                {isCollapsed ? <polyline points="6 9 12 15 18 9" /> : <polyline points="6 15 12 9 18 15" />}
              </svg>
            </button>}
          <button className="code-editor-maximize-button" onClick={toggleMaximize} title={isMaximized ? "Minimize" : "Maximize (Focus Mode)"} type="button">
            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              {isMaximized ? <><path d="M4 14h6v6" /><path d="M20 10h-6V4" /><path d="M14 10l7-7" /><path d="M3 21l7-7" /></> : <><path d="M15 3h6v6" /><path d="M9 21H3v-6" /><path d="M21 3l-7 7" /><path d="M3 21l7-7" /></>}
            </svg>
          </button>
        </div>
      </div>

      {!isCollapsed && <div style={{
    position: 'relative',
    height: isMaximized ? 'auto' : height,
    flex: isMaximized ? 1 : 'none'
  }}>
          <iframe key={iframeKey} ref={iframeRef} onLoad={handleIframeLoad} src={stackblitzUrl} className="code-editor-iframe" style={{
    height: '100%',
    flex: isMaximized ? 1 : 'none'
  }} title={title || 'Code Example'} allow="accelerometer; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" />

          {isStuck && <div className="code-editor-stuck-overlay">
              <div className="code-editor-stuck-box">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" strokeWidth="2">
                  <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
                  <line x1="12" y1="9" x2="12" y2="13"></line>
                  <line x1="12" y1="17" x2="12.01" y2="17"></line>
                </svg>
                <p>StackBlitz is taking too long to load. This can happen when the repository was recently updated.</p>
                <button type="button" className="code-editor-button" onClick={handleRetry} style={{
    marginTop: '8px'
  }}>
                  Retry
                </button>
              </div>
            </div>}
        </div>}
    </div>;
};

MCP is the open standard for connecting agents to tools. Instead of hardcoding tools inside agents, MCP servers expose them over HTTP — any agent can discover and use them.

## What is MCP?

The **Model Context Protocol (MCP)** is an open standard introduced by Anthropic in late 2024 that defines how AI agents connect to external tools and data sources. Before MCP, every agent framework invented its own way to define and call tools — leading to fragmentation and vendor lock-in.

MCP solves this with a universal protocol: tools are exposed as **MCP servers** (lightweight processes that declare their capabilities), and agents connect to them as **MCP clients**. Think of it like USB for AI — one standard interface that works everywhere.

**Why MCP became the de-facto standard:**

* **Before MCP:** Each framework (LangChain, AutoGen, Semantic Kernel) had its own tool format. Tools built for one couldn't be reused in another.
* **After MCP:** A tool built as an MCP server works with any MCP-compatible client — Claude, ChatGPT, Cursor, VS Code, or your own agent.
* **Adoption:** Within months of its release, MCP gained support from Anthropic, OpenAI, Google, Microsoft, and most major agent frameworks.

**The MCP architecture:**

```
┌─────────────┐     MCP Protocol     ┌─────────────────┐
│   Agent      │ ◄──────────────────► │   MCP Server    │
│  (Client)    │   JSON-RPC over      │  (Weather API)  │
│              │   stdio / HTTP       │                 │
└─────────────┘                       └─────────────────┘
       │                                      │
       │          MCP Protocol                │
       │ ◄──────────────────────────► ┌───────┴─────────┐
       │                              │   MCP Server    │
       │                              │  (Database)     │
       └──────────────────────────► └─────────────────┘
```

An agent can connect to multiple MCP servers simultaneously — each one exposing a different set of tools. The agent discovers available tools at runtime, selects the right ones, and calls them through the protocol.

## From Hardcoded Tools to MCP

In the [previous section](/agents/intro), you built a weather agent with a tool defined directly inside the agent code. That works for a single agent — but what happens when you want a second agent to use the same weather tool? Or when a different team wants to add a new tool without touching your agent code?

You'd have to copy the tool definition, keep them in sync, and redeploy every agent when a tool changes. This doesn't scale.

**MCP solves this by separating tools from agents.** Instead of defining tools inside your agent, you run them as independent **MCP servers** over HTTP. Any agent can connect, discover available tools, and call them — without knowing how they're implemented.

Here's what changes when you move the weather tool from hardcoded to MCP:

|                      | Hardcoded Tool (intro)            | MCP Server                             |
| -------------------- | --------------------------------- | -------------------------------------- |
| **Where tool lives** | Inside agent code                 | Separate process on `localhost:8002`   |
| **Discovery**        | Agent knows tools at compile time | Agent queries `tools/list` at runtime  |
| **Reuse**            | Copy-paste to other agents        | Any MCP client connects                |
| **Updates**          | Redeploy the agent                | Restart the server — agents pick it up |
| **Protocol**         | Framework-specific                | Standard JSON-RPC over HTTP            |

**The MCP tool lifecycle:**

1. **MCP server starts** on a port and declares its tools (name, description, parameter schema)
2. **Agent connects** as an MCP client and discovers available tools via `tools/list`
3. **User sends a query** — the LLM sees the tool descriptions and decides which to call
4. **Agent calls the tool** through the MCP protocol with structured parameters
5. **MCP server executes** and returns a structured result
6. **LLM uses the result** to continue reasoning or respond to the user

The critical insight: the LLM never sees your code — it only sees the **tool name, description, and parameter schema**. That's why tool design is everything. A poorly described tool will be misused regardless of how well it's implemented.

Here's the weather tool from the intro, now exposed as an MCP server. Notice how the tool is registered with a name, description, Zod schema, and handler — this is the standard MCP pattern:

<CodeEditor file="src/agents/weather_mcp_server.ts" lines="20-62" functionName="registerTool" title="Weather MCP Server — Tool Registration" />

The `mcp.registerTool()` call is all it takes to make a tool available over the protocol. Any MCP client that connects to this server will automatically discover `get_weather` and know how to call it.

## Tool Design Principles

### Principle 1: Clear, Descriptive Names

**Bad names:**

* `process` (process what?)
* `fetch` (fetch what?)
* `do_thing` (what thing?)

**Good names:**

```ts Pseudocode theme={null}
// MCP tool naming examples
get_customer_by_email
search_products_by_category
calculate_shipping_cost_for_order
send_notification_to_user
```

**Naming convention:** `[verb]_[noun]_[context]`

### Principle 2: Comprehensive Descriptions

The description is the most important part of your tool. It must answer:

* **What** does this tool do?
* **When** should the agent use it?
* **When NOT** to use it (distinguish from similar tools)
* **What format** are inputs/outputs?

**Bad description:**

```ts Pseudocode theme={null}
// Bad: Vague tool definition
mcp.registerTool(
    'get_data',
    {
        title: 'Get Data',
        description: 'Get data.',  // ❌ Too vague!
        inputSchema: {
            id: z.string()
        }
    },
    async (args) => { /* ... */ }
);
```

**Good description:**

```ts Pseudocode theme={null}
// Good: Comprehensive tool definition
const customerSchema = {
    customer_id: z.string().describe(
        'Customer ID in format CUST-##### (e.g., "CUST-12345")'
    )
};

mcp.registerTool(
    'get_customer_by_id',
    {
        title: 'Get Customer By ID',
        description: `Retrieve customer account information by customer ID.
        
        Use this when:
        - You have a customer ID and need their details
        - User mentions "my account" (look up by context)
        
        Do NOT use for:
        - Searching by name/email (use search_customers instead)
        - Getting order history (use get_customer_orders instead)
        
        Returns: Customer object with name, email, phone, address, account_status
        
        Example:
          Input: customer_id="CUST-12345"
          Output: { name: "Alice Johnson", email: "alice@example.com", account_status: "active" }`,
        inputSchema: customerSchema
    },
    async (args) => {
        const customer = await customerDb.findById(args.customer_id);
        return { content: [{ type: "text", text: JSON.stringify(customer) }] };
    }
);
```

### Principle 3: Simple Parameter Schemas

**Research shows:** Tool parameter complexity significantly affects agent accuracy.

| Parameter Count | Agent Accuracy       |
| --------------- | -------------------- |
| 1-3 parameters  | 90%+ correct usage   |
| 4-6 parameters  | 75-85% correct usage |
| 7+ parameters   | 60-70% correct usage |

**Why:** More parameters = more cognitive load = more confusion.

**Design principle:** Prefer multiple simple tools over one complex tool.

**Anti-pattern: Complex Tool**

```ts Pseudocode theme={null}
// Anti-pattern: Too many parameters (10) - agent will struggle
const complexOrderSchema = {
    customer_id: z.string(),
    product_ids: z.array(z.string()),
    quantities: z.array(z.number()),
    shipping_address: z.object({}),
    billing_address: z.object({}),
    payment_method: z.string(),
    promotional_code: z.string(),
    gift_wrap: z.boolean(),
    gift_message: z.string(),
    shipping_speed: z.string()
};

mcp.registerTool(
    'create_order',
    {
        title: 'Create Order',
        description: '10 parameters - agent will struggle.',
        inputSchema: complexOrderSchema
    },
    async (args) => { /* ... */ }
);
```

**Better: Multiple Simple Tools**

```ts Pseudocode theme={null}
// Better: Break into 3 simple tools (2-3 parameters each)

// Tool 1: Create cart (2 parameters)
mcp.registerTool(
    'create_order_cart',
    {
        title: 'Create Order Cart',
        description: 'Create shopping cart. Returns cart_id. Use this as first step when customer wants to place an order.',
        inputSchema: {
            customer_id: z.string(),
            items: z.array(z.object({ product_id: z.string(), quantity: z.number() }))
        }
    },
    async (args) => {
        const cartId = await createCart(args.customer_id, args.items);
        return { content: [{ type: "text", text: cartId }] };
    }
);

// Tool 2: Set shipping (3 parameters)
mcp.registerTool(
    'set_cart_shipping',
    {
        title: 'Set Cart Shipping',
        description: 'Set shipping details for cart. Call after create_order_cart, before finalize_order.',
        inputSchema: {
            cart_id: z.string(),
            address: z.object({}),
            speed: z.enum(['standard', 'express', 'overnight'])
        }
    },
    async (args) => {
        await setShipping(args.cart_id, args.address, args.speed);
        return { content: [{ type: "text", text: "Shipping set" }] };
    }
);

// Tool 3: Finalize order (2 parameters)
mcp.registerTool(
    'finalize_order',
    {
        title: 'Finalize Order',
        description: 'Complete order and charge payment. Returns order_id. Final step after cart is configured.',
        inputSchema: {
            cart_id: z.string(),
            payment_method: z.string()
        }
    },
    async (args) => {
        const orderId = await finalizeOrder(args.cart_id, args.payment_method);
        return { content: [{ type: "text", text: orderId }] };
    }
);
```

**Result:** Three simple tools have higher success rate than one complex tool, even though they require more agent steps.

*Source: "Tool Space Interference in the MCP Era" - Microsoft Research ([microsoft.com/research](https://www.microsoft.com/en-us/research/blog/tool-space-interference-in-the-mcp-era-designing-for-agent-compatibility-at-scale/))*

### Principle 4: Consistent Return Formats

**Standard response envelope:**

```ts Pseudocode theme={null}
// Standard response format for all MCP tools
interface ToolResponse {
    success: boolean;
    data?: any;
    error?: string;
    message: string;
}

mcp.registerTool(
    'example_tool',
    {
        title: 'Example Tool',
        description: 'Tool with consistent response format.',
        inputSchema: { param: z.string() }
    },
    async (args): Promise<ToolResponse> => {
        try {
            const result = await process(args.param);
            return {
                success: true,
                data: result,
                error: undefined,
                message: "Operation completed successfully"
            };
        } catch (e: any) {
            return {
                success: false,
                data: undefined,
                error: e.constructor.name,
                message: `Failed: ${e.message}`
            };
        }
    }
);
```

**Benefits:**

* Agent knows what to expect
* Easy to check success/failure
* Consistent error handling

## Build Your Own MCP Server

The weather server has one tool. A real production server has many. This customer support MCP server exposes four tools that work together — an agent connecting to it can answer FAQs, look up accounts, create tickets, and track orders, all through the same MCP protocol.

### Search Knowledge Base

The first tool an agent reaches for when a user asks a question. It searches help articles by keyword and optional category, returning matches with confidence scores. The description explicitly tells the agent *when* to use it ("general questions, how-to, troubleshooting") so it doesn't call customer lookup for a simple FAQ.

### Customer Support Agent

The full example ties everything together: three MCP servers (knowledge base, customer info, incident tickets), a LangChain agent that discovers tools from all of them, and thread-based memory so the conversation persists across turns.

```
┌──────────────────────┐
│  CustomerSupportAgent │  ← LangChain + MemorySaver (thread_id)
│  (one per user)       │
└──────────┬───────────┘
           │  discovers tools via MCP
     ┌─────┼──────────────┐
     ▼     ▼              ▼
┌─────────┐ ┌────────────┐ ┌──────────────┐
│Knowledge│ │ Customer   │ │ Incident     │
│Base     │ │ Info       │ │ Ticket       │
│ :8001   │ │ :8002      │ │ :8003        │
└─────────┘ └────────────┘ └──────────────┘
  1 tool      4 tools        2 tools
```

Each server runs independently, owns its domain, and can be deployed/scaled separately. The agent doesn't know or care where the tools live — it discovers them all via `tools/list` and the LLM picks the right one per query.

<CodeEditor file="src/agents/customer_support_agent.ts" lines="26-67" functionName="main" title="Customer Support Agent — 3 MCP Servers" />

<Quiz>
  <QuizQuestion question="Your agent has a hardcoded tool that works perfectly. Why would you move it to an MCP server?" options={["For better performance — MCP is faster than direct function calls", "For reusability — any MCP-compatible agent can discover and use it without code changes", "MCP is required by all LLM providers"]} answer={1} explanation="MCP's value is decoupling tools from agents. A tool running as an MCP server can be used by Claude, ChatGPT, Cursor, or your own agents — without copying code or changing implementations." />

  <QuizQuestion question="An agent calls tools/list on your MCP server and gets back 15 tools. The agent then picks the wrong tool for the user's query. What's the most likely cause?" options={["Too many tools — the agent can't handle more than 5", "Poor tool descriptions — the LLM only sees names and descriptions, not your implementation", "The MCP protocol lost data during transmission"]} answer={1} explanation="The LLM never sees your code. It chooses tools based solely on names, descriptions, and parameter schemas. Vague or overlapping descriptions cause misselection regardless of how well the tools are implemented." />
</Quiz>
