Skip to content
Published on

Build Your Own MCP Server: Protocol Internals, Hands-On Guide, and 100+ Ecosystem Deep Dive

Authors

1. Dissecting MCP Protocol Internals

MCP (Model Context Protocol) is a standard protocol for connecting AI models with external systems. In this post, we go beyond the introductory overview and deeply examine the internal workings of the protocol, then walk through building MCP servers from scratch.

1-1. JSON-RPC 2.0 Message Structure

All MCP communication follows the JSON-RPC 2.0 specification. Messages fall into three types.

Request: Sent when a client or server asks the other side to perform an operation.

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {
      "city": "Seoul"
    }
  }
}

Response: Returns the result for a request.

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Seoul: 15 degrees, partly cloudy"
      }
    ]
  }
}

Notification: A one-way message that expects no response. Note the absence of an id field.

{
  "jsonrpc": "2.0",
  "method": "notifications/initialized"
}

Error Response: Returned when request processing fails.

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32602,
    "message": "Invalid params: city is required"
  }
}

Key error codes:

CodeMeaningDescription
-32700Parse ErrorFailed to parse JSON
-32600Invalid RequestMalformed request
-32601Method Not FoundNon-existent method
-32602Invalid ParamsInvalid parameters
-32603Internal ErrorServer internal error

1-2. Connection Lifecycle

An MCP connection goes through clearly defined phases:

Phase 1 - Initialize

The client sends an initialize request, exchanging protocol versions and capabilities with the server.

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-03-26",
    "capabilities": {
      "roots": {
        "listChanged": true
      },
      "sampling": {}
    },
    "clientInfo": {
      "name": "Claude Desktop",
      "version": "1.5.0"
    }
  }
}

The server responds:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-03-26",
    "capabilities": {
      "tools": {
        "listChanged": true
      },
      "resources": {
        "subscribe": true,
        "listChanged": true
      },
      "prompts": {
        "listChanged": true
      }
    },
    "serverInfo": {
      "name": "Weather MCP Server",
      "version": "1.0.0"
    }
  }
}

Phase 2 - Initialized

The client sends a notifications/initialized notification to declare initialization is complete.

{
  "jsonrpc": "2.0",
  "method": "notifications/initialized"
}

Phase 3 - Normal Operation

Client and server can now freely exchange messages:

  • Client requests available tools via tools/list
  • Client invokes a specific tool via tools/call
  • Client reads resources via resources/read
  • Server notifies about tool list changes via notifications/tools/list_changed

Phase 4 - Shutdown

When the client terminates the connection, the transport layer cleans up.

1-3. Transport Methods

MCP supports two transport methods.

stdio (Standard Input/Output)

The most common method, running MCP server processes locally.

  • Client spawns the server process
  • Sends requests via stdin, receives responses via stdout
  • Each message is delimited by newlines
  • Debug logs go to stderr
  • Used primarily with Claude Desktop and Claude Code
Client                    Server Process
  |                           |
  |--- spawn process -------->|
  |                           |
  |--- stdin: JSON-RPC ------>|
  |<-- stdout: JSON-RPC -----|
  |                           |
  |--- stdin: JSON-RPC ------>|
  |<-- stdout: JSON-RPC -----|
  |                           |
  |--- kill process --------->|

Streamable HTTP

Used for connecting to remote servers. The March 2025 spec update replaced the older HTTP+SSE approach.

  • Requests sent via HTTP POST
  • Server streams responses via SSE (Server-Sent Events)
  • Suitable for remote deployment and multi-tenant scenarios
  • Uses Mcp-Session-Id header for session management
Client                    HTTP Server
  |                           |
  |--- POST /mcp ----------->|
  |    (initialize)           |
  |<-- SSE: result -----------|
  |<-- Mcp-Session-Id --------|
  |                           |
  |--- POST /mcp ----------->|
  |    (tools/call)           |
  |<-- SSE: result -----------|
  |                           |
  |--- DELETE /mcp ---------->|
  |    (session close)        |

Which Transport to Choose?

CriteriastdioStreamable HTTP
Use CaseLocal dev, desktop appsRemote servers, cloud deployment
Setup DifficultyVery easyModerate
SecurityLocal process isolationRequires OAuth 2.0, TLS
Multi-userNot possiblePossible
NetworkNot requiredRequired
ExampleClaude DesktopTeam-shared MCP server

1-4. Three Core Primitives

MCP servers expose three types of capabilities to AI.

Resources - Read-only Data

Provides data identified by URIs. This includes file contents, DB schemas, API responses, and more.

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "resources/read",
  "params": {
    "uri": "file:///project/schema.sql"
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "contents": [
      {
        "uri": "file:///project/schema.sql",
        "mimeType": "text/plain",
        "text": "CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100));"
      }
    ]
  }
}

Characteristics:

  • Application uses as context (user-controlled)
  • URI-based identification (file://, db://, api://, etc.)
  • Can subscribe for change notifications
  • Supports text or binary (base64) content

Tools - Executable Functions

Defines functions that AI models can invoke. Input parameters are specified using JSON Schema.

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/list"
}

Response:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "tools": [
      {
        "name": "get_weather",
        "description": "Get current weather for a city",
        "inputSchema": {
          "type": "object",
          "properties": {
            "city": {
              "type": "string",
              "description": "City name"
            }
          },
          "required": ["city"]
        }
      }
    ]
  }
}

Characteristics:

  • Model autonomously invokes (model-controlled)
  • JSON Schema validates parameter types
  • May have side effects
  • May require user confirmation (security)

Prompts - Reusable Prompt Templates

Pre-defines frequently used prompt patterns.

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "prompts/get",
  "params": {
    "name": "code_review",
    "arguments": {
      "language": "python"
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "description": "Code review prompt for Python",
    "messages": [
      {
        "role": "user",
        "content": {
          "type": "text",
          "text": "Please review the following Python code for best practices, bugs, and performance issues."
        }
      }
    ]
  }
}

1-5. Capability Negotiation Details

During initialization, client and server exchange their supported features. Servers only need to declare the primitives they intend to provide.

Server Capabilities Example (Tools only):

{
  "capabilities": {
    "tools": {
      "listChanged": true
    }
  }
}

This server only provides Tools and can notify when the tool list changes. It does not provide Resources or Prompts.

Full-featured Server:

{
  "capabilities": {
    "tools": { "listChanged": true },
    "resources": { "subscribe": true, "listChanged": true },
    "prompts": { "listChanged": true },
    "logging": {}
  }
}

Client Capabilities Example:

{
  "capabilities": {
    "roots": { "listChanged": true },
    "sampling": {}
  }
}
  • roots: Client provides information about working directories/projects
  • sampling: Server can reverse-invoke the client's LLM

1-6. Sampling: Reverse LLM Invocation from Server

Sampling is a unique MCP feature that allows servers to send requests back to the client's AI model.

{
  "jsonrpc": "2.0",
  "id": 10,
  "method": "sampling/createMessage",
  "params": {
    "messages": [
      {
        "role": "user",
        "content": {
          "type": "text",
          "text": "Summarize this error log: Connection timeout after 30s..."
        }
      }
    ],
    "maxTokens": 500,
    "modelPreferences": {
      "hints": [{ "name": "claude-sonnet-4-20250514" }]
    }
  }
}

Use cases:

  • Server asks AI to analyze collected log data
  • Server asks AI to summarize complex data
  • Intermediate judgment requests in agent workflows

Note that Sampling requires user confirmation for security. The client enforces human-in-the-loop control.


2. Building an MCP Server with Python FastMCP (Hands-On 1)

FastMCP is the easiest way to build MCP servers in Python. It is also the official high-level API for the MCP Python SDK.

2-1. Weather MCP Server (Simplest Example)

Installation:

pip install fastmcp

weather_server.py:

from fastmcp import FastMCP

# Create MCP server instance
mcp = FastMCP("Weather Server")


@mcp.tool()
def get_weather(city: str) -> str:
    """Get current weather for a city.

    Args:
        city: Name of the city (e.g., Seoul, Tokyo, New York)
    """
    # In production, you would call a weather API
    weather_data = {
        "Seoul": "15 degrees, partly cloudy",
        "Tokyo": "18 degrees, sunny",
        "New York": "10 degrees, rainy",
        "London": "8 degrees, foggy",
    }
    result = weather_data.get(city, f"Weather data not available for {city}")
    return f"Weather in {city}: {result}"


@mcp.tool()
def get_forecast(city: str, days: int = 3) -> str:
    """Get weather forecast for a city.

    Args:
        city: Name of the city
        days: Number of days to forecast (1-7)
    """
    if days < 1 or days > 7:
        return "Days must be between 1 and 7"
    return f"{days}-day forecast for {city}: Mostly sunny with temperatures 10-20C"


@mcp.resource("weather://cities")
def list_cities() -> str:
    """List all supported cities."""
    cities = ["Seoul", "Tokyo", "New York", "London", "Paris"]
    return "\n".join(cities)


if __name__ == "__main__":
    mcp.run()

Running the server:

# Run directly (stdio mode)
python weather_server.py

# Or use FastMCP CLI
fastmcp run weather_server.py

# Test with MCP Inspector
fastmcp dev weather_server.py

Claude Desktop integration (claude_desktop_config.json):

{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["/path/to/weather_server.py"]
    }
  }
}

Claude Code integration (.mcp.json):

{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["weather_server.py"]
    }
  }
}

2-2. Database MCP Server (Production-Ready)

Let's build a database MCP server, one of the most commonly used patterns in practice.

import sqlite3
import json
from fastmcp import FastMCP

mcp = FastMCP("Database Server")

DB_PATH = "myapp.db"


def get_connection():
    """Get database connection."""
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn


@mcp.resource("db://schema")
def get_schema() -> str:
    """Get the database schema information."""
    conn = get_connection()
    cursor = conn.cursor()
    cursor.execute(
        "SELECT sql FROM sqlite_master WHERE type='table' ORDER BY name"
    )
    tables = cursor.fetchall()
    conn.close()
    schema_lines = []
    for table in tables:
        if table[0]:
            schema_lines.append(table[0])
    return "\n\n".join(schema_lines)


@mcp.resource("db://tables")
def list_tables() -> str:
    """List all tables in the database."""
    conn = get_connection()
    cursor = conn.cursor()
    cursor.execute(
        "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
    )
    tables = [row[0] for row in cursor.fetchall()]
    conn.close()
    return json.dumps(tables, indent=2)


@mcp.tool()
def query_database(sql: str) -> str:
    """Execute a read-only SQL query and return results.

    Args:
        sql: SQL SELECT query to execute
    """
    # Security: Only allow SELECT
    if not sql.strip().upper().startswith("SELECT"):
        return "Error: Only SELECT queries are allowed for safety"

    conn = get_connection()
    try:
        cursor = conn.cursor()
        cursor.execute(sql)
        rows = cursor.fetchall()
        if not rows:
            return "No results found"

        columns = [description[0] for description in cursor.description]
        results = []
        for row in rows:
            results.append(dict(zip(columns, row)))

        return json.dumps(results, indent=2, ensure_ascii=False)
    except Exception as e:
        return f"Query error: {str(e)}"
    finally:
        conn.close()


@mcp.tool()
def insert_record(table: str, data: str) -> str:
    """Insert a record into a table.

    Args:
        table: Table name
        data: JSON string of column-value pairs
    """
    try:
        record = json.loads(data)
    except json.JSONDecodeError:
        return "Error: Invalid JSON data"

    columns = ", ".join(record.keys())
    placeholders = ", ".join(["?" for _ in record])
    values = list(record.values())

    conn = get_connection()
    try:
        cursor = conn.cursor()
        cursor.execute(
            f"INSERT INTO {table} ({columns}) VALUES ({placeholders})",
            values,
        )
        conn.commit()
        return f"Successfully inserted record with id {cursor.lastrowid}"
    except Exception as e:
        return f"Insert error: {str(e)}"
    finally:
        conn.close()


@mcp.tool()
def update_record(table: str, record_id: int, data: str) -> str:
    """Update a record in a table.

    Args:
        table: Table name
        record_id: ID of the record to update
        data: JSON string of column-value pairs to update
    """
    try:
        updates = json.loads(data)
    except json.JSONDecodeError:
        return "Error: Invalid JSON data"

    set_clause = ", ".join([f"{k} = ?" for k in updates.keys()])
    values = list(updates.values()) + [record_id]

    conn = get_connection()
    try:
        cursor = conn.cursor()
        cursor.execute(
            f"UPDATE {table} SET {set_clause} WHERE id = ?", values
        )
        conn.commit()
        return f"Successfully updated {cursor.rowcount} record(s)"
    except Exception as e:
        return f"Update error: {str(e)}"
    finally:
        conn.close()


@mcp.tool()
def delete_record(table: str, record_id: int) -> str:
    """Delete a record from a table.

    Args:
        table: Table name
        record_id: ID of the record to delete
    """
    conn = get_connection()
    try:
        cursor = conn.cursor()
        cursor.execute(f"DELETE FROM {table} WHERE id = ?", [record_id])
        conn.commit()
        return f"Successfully deleted {cursor.rowcount} record(s)"
    except Exception as e:
        return f"Delete error: {str(e)}"
    finally:
        conn.close()


if __name__ == "__main__":
    mcp.run()

This server provides:

  • Resources: DB schema information and table listings
  • Tools: SELECT queries, INSERT, UPDATE, DELETE operations

Tell Claude "Show me the 10 most recently registered users" and it will automatically invoke the query_database tool.

2-3. Filesystem MCP Server

import os
from pathlib import Path
from fastmcp import FastMCP

mcp = FastMCP("Filesystem Server")

# Security: Only allow access to specific directories
ALLOWED_DIRS = [
    Path("/Users/dev/projects"),
    Path("/Users/dev/documents"),
]


def is_path_allowed(path: str) -> bool:
    """Check if the path is within allowed directories."""
    resolved = Path(path).resolve()
    return any(
        resolved == allowed or allowed in resolved.parents
        for allowed in ALLOWED_DIRS
    )


@mcp.tool()
def read_file(path: str) -> str:
    """Read contents of a file.

    Args:
        path: Absolute file path
    """
    if not is_path_allowed(path):
        return "Error: Access denied. Path is outside allowed directories."

    try:
        with open(path, "r", encoding="utf-8") as f:
            return f.read()
    except FileNotFoundError:
        return f"Error: File not found: {path}"
    except UnicodeDecodeError:
        return "Error: File is not a text file"


@mcp.tool()
def write_file(path: str, content: str) -> str:
    """Write content to a file.

    Args:
        path: Absolute file path
        content: Content to write
    """
    if not is_path_allowed(path):
        return "Error: Access denied. Path is outside allowed directories."

    try:
        with open(path, "w", encoding="utf-8") as f:
            f.write(content)
        return f"Successfully wrote to {path}"
    except Exception as e:
        return f"Error writing file: {str(e)}"


@mcp.tool()
def list_directory(path: str) -> str:
    """List contents of a directory.

    Args:
        path: Absolute directory path
    """
    if not is_path_allowed(path):
        return "Error: Access denied. Path is outside allowed directories."

    try:
        entries = []
        for entry in sorted(Path(path).iterdir()):
            entry_type = "dir" if entry.is_dir() else "file"
            size = entry.stat().st_size if entry.is_file() else 0
            entries.append(f"[{entry_type}] {entry.name} ({size} bytes)")
        return "\n".join(entries)
    except FileNotFoundError:
        return f"Error: Directory not found: {path}"


@mcp.tool()
def search_files(directory: str, pattern: str) -> str:
    """Search for files matching a glob pattern.

    Args:
        directory: Directory to search in
        pattern: Glob pattern (e.g., '*.py', '**/*.md')
    """
    if not is_path_allowed(directory):
        return "Error: Access denied."

    matches = list(Path(directory).glob(pattern))
    if not matches:
        return "No files found matching the pattern"
    return "\n".join(str(m) for m in matches[:50])


@mcp.resource("fs://allowed-dirs")
def get_allowed_dirs() -> str:
    """List allowed directories."""
    return "\n".join(str(d) for d in ALLOWED_DIRS)


if __name__ == "__main__":
    mcp.run()

Key security points:

  • ALLOWED_DIRS restricts accessible directories
  • Path.resolve() prevents symlink bypass attacks
  • Parent directory traversal (../) is blocked

3. Building an MCP Server with TypeScript SDK (Hands-On 2)

TypeScript is the second most popular language for MCP server development. The official @modelcontextprotocol/sdk package is available.

3-1. Basic Structure

Installation:

npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "declaration": true
  },
  "include": ["src/**/*"]
}

src/index.ts (basic structure):

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'

// Create server instance
const server = new McpServer({
  name: 'My MCP Server',
  version: '1.0.0',
})

// Register a Tool
server.tool(
  'hello',
  'Say hello to someone',
  {
    name: z.string().describe('Name of the person'),
  },
  async ({ name }) => {
    return {
      content: [
        {
          type: 'text',
          text: `Hello, ${name}! Welcome to MCP.`,
        },
      ],
    }
  }
)

// Register a Resource
server.resource('info', 'server://info', async (uri) => {
  return {
    contents: [
      {
        uri: uri.href,
        mimeType: 'text/plain',
        text: 'This is My MCP Server v1.0.0',
      },
    ],
  }
})

// Connect transport and run
async function main() {
  const transport = new StdioServerTransport()
  await server.connect(transport)
  console.error('MCP Server running on stdio')
}

main().catch(console.error)

Run:

npx tsc
node dist/index.js

Zod-based parameter validation is the key advantage of the TypeScript SDK. You get both type safety and automatic JSON Schema generation.

3-2. GitHub Issue MCP Server (Production)

Let's build a practical GitHub issue management MCP server.

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'

const server = new McpServer({
  name: 'GitHub Issue Server',
  version: '1.0.0',
})

const GITHUB_TOKEN = process.env.GITHUB_TOKEN
const BASE_URL = 'https://api.github.com'

// Common headers
function getHeaders() {
  return {
    Authorization: `Bearer ${GITHUB_TOKEN}`,
    Accept: 'application/vnd.github.v3+json',
    'Content-Type': 'application/json',
    'User-Agent': 'MCP-GitHub-Server',
  }
}

// Tool: List issues
server.tool(
  'list_issues',
  'List issues in a GitHub repository',
  {
    owner: z.string().describe('Repository owner'),
    repo: z.string().describe('Repository name'),
    state: z.enum(['open', 'closed', 'all']).default('open').describe('Issue state filter'),
    labels: z.string().optional().describe('Comma-separated label names'),
    per_page: z.number().min(1).max(100).default(10).describe('Results per page'),
  },
  async ({ owner, repo, state, labels, per_page }) => {
    const params = new URLSearchParams({
      state,
      per_page: String(per_page),
    })
    if (labels) params.set('labels', labels)

    const response = await fetch(`${BASE_URL}/repos/${owner}/${repo}/issues?${params}`, {
      headers: getHeaders(),
    })

    if (!response.ok) {
      return {
        content: [
          {
            type: 'text' as const,
            text: `Error: ${response.status} ${response.statusText}`,
          },
        ],
      }
    }

    const issues = await response.json()
    const formatted = issues.map(
      (issue: Record<string, unknown>) =>
        `#${issue.number} [${issue.state}] ${issue.title}\n  Labels: ${
          (issue.labels as Array<Record<string, string>>).map((l) => l.name).join(', ') || 'none'
        }\n  Created: ${issue.created_at}`
    )

    return {
      content: [
        {
          type: 'text' as const,
          text: formatted.join('\n\n') || 'No issues found',
        },
      ],
    }
  }
)

// Tool: Create issue
server.tool(
  'create_issue',
  'Create a new issue in a GitHub repository',
  {
    owner: z.string().describe('Repository owner'),
    repo: z.string().describe('Repository name'),
    title: z.string().describe('Issue title'),
    body: z.string().optional().describe('Issue body (Markdown)'),
    labels: z.array(z.string()).optional().describe('Labels to add'),
    assignees: z.array(z.string()).optional().describe('Usernames to assign'),
  },
  async ({ owner, repo, title, body, labels, assignees }) => {
    const response = await fetch(`${BASE_URL}/repos/${owner}/${repo}/issues`, {
      method: 'POST',
      headers: getHeaders(),
      body: JSON.stringify({ title, body, labels, assignees }),
    })

    if (!response.ok) {
      const error = await response.text()
      return {
        content: [
          {
            type: 'text' as const,
            text: `Error creating issue: ${response.status} - ${error}`,
          },
        ],
      }
    }

    const issue = await response.json()
    return {
      content: [
        {
          type: 'text' as const,
          text: `Created issue #${issue.number}: ${issue.title}\nURL: ${issue.html_url}`,
        },
      ],
    }
  }
)

// Tool: Close issue
server.tool(
  'close_issue',
  'Close an issue in a GitHub repository',
  {
    owner: z.string().describe('Repository owner'),
    repo: z.string().describe('Repository name'),
    issue_number: z.number().describe('Issue number'),
    comment: z.string().optional().describe('Optional closing comment'),
  },
  async ({ owner, repo, issue_number, comment }) => {
    if (comment) {
      await fetch(`${BASE_URL}/repos/${owner}/${repo}/issues/${issue_number}/comments`, {
        method: 'POST',
        headers: getHeaders(),
        body: JSON.stringify({ body: comment }),
      })
    }

    const response = await fetch(`${BASE_URL}/repos/${owner}/${repo}/issues/${issue_number}`, {
      method: 'PATCH',
      headers: getHeaders(),
      body: JSON.stringify({ state: 'closed' }),
    })

    if (!response.ok) {
      return {
        content: [
          {
            type: 'text' as const,
            text: `Error closing issue: ${response.status}`,
          },
        ],
      }
    }

    return {
      content: [
        {
          type: 'text' as const,
          text: `Closed issue #${issue_number} successfully`,
        },
      ],
    }
  }
)

// Resource: Repository info
server.resource('repo-info', 'github://repo-info', async (uri) => {
  return {
    contents: [
      {
        uri: uri.href,
        mimeType: 'text/plain',
        text: 'GitHub Issue MCP Server - Manage issues via AI',
      },
    ],
  }
})

// Prompt: Issue triage
server.prompt(
  'triage_issues',
  'Generate a prompt for triaging open issues',
  { owner: z.string(), repo: z.string() },
  ({ owner, repo }) => ({
    messages: [
      {
        role: 'user' as const,
        content: {
          type: 'text' as const,
          text: `Please triage the open issues in ${owner}/${repo}. For each issue:
1. Assess priority (critical/high/medium/low)
2. Suggest appropriate labels
3. Recommend if it should be assigned to someone
4. Provide a brief action plan`,
        },
      },
    ],
  })
)

async function main() {
  const transport = new StdioServerTransport()
  await server.connect(transport)
  console.error('GitHub Issue MCP Server running')
}

main().catch(console.error)

package.json:

{
  "name": "github-issue-mcp",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.12.0",
    "zod": "^3.24.0"
  },
  "devDependencies": {
    "typescript": "^5.7.0",
    "@types/node": "^22.0.0"
  }
}

Claude Desktop integration:

{
  "mcpServers": {
    "github-issues": {
      "command": "node",
      "args": ["/path/to/github-issue-mcp/dist/index.js"],
      "env": {
        "GITHUB_TOKEN": "ghp_your_token_here"
      }
    }
  }
}

4. Testing and Debugging MCP Servers

4-1. MCP Inspector

MCP Inspector is the official tool for locally testing MCP servers.

# Run directly with npx
npx @modelcontextprotocol/inspector

# Run with a specific server
npx @modelcontextprotocol/inspector python weather_server.py

# FastMCP dev mode (auto-connects Inspector)
fastmcp dev weather_server.py

Inspector capabilities:

  • Browse all tools, resources, and prompts
  • Invoke tools and inspect results
  • View JSON-RPC message logs in real-time
  • Monitor connection status

4-2. Claude Desktop Integration

Configuration file location on macOS:

# Open config file
code ~/Library/Application\ Support/Claude/claude_desktop_config.json

Windows:

%APPDATA%\Claude\claude_desktop_config.json

Configuration example:

{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["/absolute/path/to/weather_server.py"]
    },
    "database": {
      "command": "python",
      "args": ["/absolute/path/to/db_server.py"],
      "env": {
        "DB_PATH": "/data/myapp.db"
      }
    },
    "github": {
      "command": "node",
      "args": ["/absolute/path/to/github-mcp/dist/index.js"],
      "env": {
        "GITHUB_TOKEN": "ghp_xxx"
      }
    }
  }
}

4-3. Claude Code Integration

Create a .mcp.json file at the project root:

{
  "mcpServers": {
    "my-server": {
      "command": "python",
      "args": ["./tools/my_mcp_server.py"]
    }
  }
}

Or use global settings:

claude mcp add my-server python ./tools/my_mcp_server.py

4-4. Logging Strategy

Debug logs in MCP servers should go to stderr. Stdout is reserved exclusively for JSON-RPC messages.

Python:

import sys
import logging

# Configure logging to stderr
logging.basicConfig(
    level=logging.DEBUG,
    stream=sys.stderr,
    format="%(asctime)s [%(levelname)s] %(message)s",
)

logger = logging.getLogger("my-mcp-server")

@mcp.tool()
def my_tool(param: str) -> str:
    logger.debug(f"Tool called with param: {param}")
    try:
        result = do_something(param)
        logger.info(f"Tool succeeded: {result}")
        return result
    except Exception as e:
        logger.error(f"Tool failed: {e}")
        return f"Error: {str(e)}"

TypeScript:

// Log to stderr
function log(level: string, message: string) {
  console.error(`[${new Date().toISOString()}] [${level}] ${message}`)
}

server.tool('my_tool', 'Description', { param: z.string() }, async ({ param }) => {
  log('DEBUG', `Tool called with: ${param}`)
  try {
    const result = await doSomething(param)
    log('INFO', `Success: ${result}`)
    return { content: [{ type: 'text', text: result }] }
  } catch (error) {
    log('ERROR', `Failed: ${error}`)
    return { content: [{ type: 'text', text: `Error: ${error}` }] }
  }
})

4-5. Error Handling Patterns

from fastmcp import FastMCP
from fastmcp.exceptions import ToolError

mcp = FastMCP("Robust Server")

@mcp.tool()
def safe_operation(param: str) -> str:
    """A tool with proper error handling."""
    # Input validation
    if not param or len(param) > 1000:
        raise ToolError("Parameter must be 1-1000 characters")

    try:
        result = external_api_call(param)
        return result
    except ConnectionError:
        raise ToolError("Failed to connect to external service. Please try again.")
    except TimeoutError:
        raise ToolError("Request timed out. The service may be temporarily unavailable.")
    except Exception as e:
        # Hide details for unexpected errors, return generic message
        logging.error(f"Unexpected error: {e}", exc_info=True)
        raise ToolError("An unexpected error occurred. Check server logs for details.")

5. Deploying MCP Servers

5-1. Cloudflare Workers Deployment

Cloudflare Workers lets you deploy MCP servers serverlessly across the globe.

# Create project
npm create cloudflare@latest -- my-mcp-server
cd my-mcp-server
npm install @modelcontextprotocol/sdk

src/index.ts (Workers version):

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'

export default {
  async fetch(request: Request): Promise<Response> {
    const server = new McpServer({
      name: 'Cloudflare MCP Server',
      version: '1.0.0',
    })

    // Register Tools
    server.tool('hello', 'Say hello', { name: z.string() }, async ({ name }) => ({
      content: [{ type: 'text', text: `Hello ${name} from the edge!` }],
    }))

    // Handle Streamable HTTP transport
    // In practice, use the MCP SDK's HTTP transport handler
    return new Response('MCP Server Running', { status: 200 })
  },
}
# Deploy
npx wrangler deploy

5-2. Docker Containerization

FROM node:22-slim

WORKDIR /app

COPY package*.json ./
RUN npm ci --production

COPY dist/ ./dist/

# MCP server communicates via stdio
CMD ["node", "dist/index.js"]
docker build -t my-mcp-server .
docker run -i my-mcp-server

Connect Docker server from Claude Desktop:

{
  "mcpServers": {
    "my-server": {
      "command": "docker",
      "args": ["run", "-i", "--rm", "my-mcp-server"]
    }
  }
}

5-3. npm/PyPI Package Distribution

npm package:

{
  "name": "@myorg/mcp-weather",
  "version": "1.0.0",
  "bin": {
    "mcp-weather": "./dist/index.js"
  }
}
npm publish --access public

Users can instantly use it:

{
  "mcpServers": {
    "weather": {
      "command": "npx",
      "args": ["-y", "@myorg/mcp-weather"]
    }
  }
}

PyPI package:

pip install build twine
python -m build
twine upload dist/*
{
  "mcpServers": {
    "weather": {
      "command": "uvx",
      "args": ["mcp-weather"]
    }
  }
}

5-4. Smithery Marketplace

Smithery (smithery.ai) is a dedicated MCP server registry. Registration process:

  1. Push MCP server code to a GitHub repository
  2. Add a smithery.yaml configuration file
  3. Connect the repo on the Smithery website
  4. Automatic build and deployment
# smithery.yaml
name: my-weather-mcp
description: Weather MCP server for AI assistants
icon: cloud-sun
startCommand:
  type: stdio
  configSchema:
    type: object
    properties:
      API_KEY:
        type: string
        description: Weather API key
    required:
      - API_KEY
  commandFunction:
    - node
    - dist/index.js

5-5. Adding OAuth 2.0 Authentication

Authentication is essential for remote MCP servers. OAuth 2.0 support was formalized in the June 2025 spec.

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'

const server = new McpServer({
  name: 'Authenticated Server',
  version: '1.0.0',
})

// OAuth 2.0 middleware (Streamable HTTP transport)
async function validateToken(token: string): Promise<boolean> {
  const response = await fetch('https://auth.example.com/introspect', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: `token=${token}`,
  })
  const data = await response.json()
  return data.active === true
}

// Authenticate in HTTP handler
async function handleRequest(req: Request): Promise<Response> {
  const authHeader = req.headers.get('Authorization')
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return new Response('Unauthorized', { status: 401 })
  }

  const token = authHeader.slice(7)
  if (!(await validateToken(token))) {
    return new Response('Invalid token', { status: 403 })
  }

  // Continue processing MCP request...
  return new Response('OK')
}

6. 100+ MCP Server Ecosystem Roundup

As of March 2026, the MCP ecosystem is growing explosively. Here is a categorized overview of major servers.

6-1. Data and Analytics

ServerFeaturesUse Case
Grafana MCPDashboard queries, metric queries, alert managementAI-driven monitoring automation
PostgreSQL MCPSQL queries, schema exploration, data analysisNatural language to SQL
MongoDB MCPCollection CRUD, aggregation pipelinesNoSQL data exploration
ClickHouse MCPAnalytical queries, log retrievalLog-based troubleshooting
BigQuery MCPLarge-scale data analysisData team productivity
MySQL MCPSQL queries, table managementRDBMS operations
Redis MCPCache inspection, key-value managementCache debugging
Elasticsearch MCPFull-text search, index managementLog analysis automation
Snowflake MCPData warehouse queriesBusiness intelligence
DuckDB MCPLocal analytical queriesFast data exploration
Prometheus MCPMetric queries (PromQL)System monitoring
Datadog MCPMetrics, dashboards, alertsUnified observability

Grafana MCP Example:

User: "Find servers where CPU usage exceeded 80% in the last 24 hours"
Claude: [Calls Grafana MCP query_metrics tool]
        -> PromQL: avg by (instance)(cpu_usage_percent > 80)
        -> Results: server-03 (avg 87%), server-07 (avg 92%)

6-2. Developer Tools

ServerFeaturesUse Case
GitHub MCPIssues, PRs, Actions, code searchCode review automation
GitLab MCPPipelines, merge requestsCI/CD management
Jira MCPIssue tracking, sprint managementPM workflow automation
Linear MCPIssue/project managementModern issue tracker
Sentry MCPError tracking, performance monitoringDebugging automation
CircleCI MCPCI/CD pipeline managementBuild monitoring
npm MCPPackage search, version checksDependency management
Snyk MCPSecurity vulnerability scanningSecurity audit automation
SonarQube MCPCode quality analysisCode review assistance
Postman MCPAPI testing, collection managementAPI development efficiency

6-3. Documentation and Visualization

ServerFeaturesUse Case
Mermaid MCPDiagram generation (flowchart, sequence, ER)Documentation automation
PPT/Slides MCPPresentation creation/editingReport automation
Notion MCPPages, database CRUDKnowledge management
Google Docs MCPDocument read/editCollaborative documentation
Confluence MCPWiki page managementTechnical documentation
Excalidraw MCPWhiteboard diagramsArchitecture diagrams
Google Sheets MCPSpreadsheet read/editData reports
Obsidian MCPMarkdown note managementPersonal knowledge base

6-4. Communication

ServerFeaturesUse Case
Slack MCPChannel messages, search, DMsTeam communication automation
Discord MCPServer/channel managementCommunity management
Email MCPRead/send/search emailsEmail automation
Microsoft Teams MCPChat, meeting managementEnterprise collaboration
Telegram MCPBot messages, channel managementNotification automation
Twilio MCPSMS, voice callsCustomer communication

6-5. Cloud and Infrastructure

ServerFeaturesUse Case
AWS MCPS3, Lambda, EC2 managementInfrastructure automation
Kubernetes MCPCluster management, Pod inspectionK8s operations
Docker MCPContainer/image managementDev environment management
Terraform MCPIaC execution, state inspectionInfrastructure code generation
Vercel MCPDeployment, env vars, logsFrontend deployment
GCP MCPGoogle Cloud service managementGCP infrastructure automation
Azure MCPAzure service managementEnterprise cloud
Cloudflare MCPWorkers, DNS, CDNEdge computing management
Pulumi MCPIaC (programming languages)Code-based infrastructure
Ansible MCPConfiguration managementServer provisioning

6-6. AI and ML

ServerFeaturesUse Case
HuggingFace MCPModel search, inferenceML workflows
Weights and Biases MCPExperiment tracking, metricsML experiment management
LangChain MCPChain/agent executionAI pipelines
Ollama MCPLocal LLM managementPrivate AI execution
Replicate MCPCloud model executionML model deployment
Pinecone MCPVector DB managementRAG implementation
Weaviate MCPVector searchSemantic search
ChromaDB MCPLocal vector DBEmbedding management

6-7. Browser and Scraping

ServerFeaturesUse Case
Playwright MCPBrowser automation, screenshotsWeb testing
Puppeteer MCPChrome automationCrawling
Brave Search MCPWeb search APIReal-time information for AI
Firecrawl MCPWebsite crawling, markdown conversionRAG data collection
Browserbase MCPCloud browsersLarge-scale web automation
Exa MCPSemantic web searchHigh-quality search results
Tavily MCPAI-optimized searchResearch automation

6-8. Files and Storage

ServerFeaturesUse Case
Filesystem MCPLocal file read/writeFile management
Google Drive MCPFile search/downloadCloud file management
S3 MCPBucket/object managementCloud storage
Dropbox MCPFile sync, sharingFile collaboration
OneDrive MCPMicrosoft file managementEnterprise files
Box MCPEnterprise file managementEnterprise content management

6-9. Design and Media

ServerFeaturesUse Case
Figma MCPDesign file viewing, component extractionDesign-to-code conversion
Canva MCPDesign creation/editingMarketing materials
FFmpeg MCPAudio/video conversionMedia processing
Sharp MCPImage processingImage optimization

6-10. Specialized Servers

ServerFeaturesUse Case
Stripe MCPPayments, subscription managementPayment systems
Shopify MCPStore, product managementE-commerce
SendGrid MCPEmail deliveryMarketing emails
Airtable MCPSpreadsheet DBNo-code data management
Supabase MCPBaaS (DB, Auth, Storage)Full-stack development
Firebase MCPGoogle BaaSMobile/web backends
Neon MCPServerless PostgreSQLDatabase management
PlanetScale MCPServerless MySQLScalable DB
Turso MCPEdge SQLiteDistributed database

7. 10 Ideas for Your Own MCP Server

Building your own MCP server dramatically expands AI's usefulness. Here are practical ideas.

7-1. Personal Blog MCP

An MCP server for managing MDX-file-based blogs.

from fastmcp import FastMCP
import glob
import yaml

mcp = FastMCP("Blog Manager")

BLOG_DIR = "/path/to/blog/data"

@mcp.tool()
def list_posts(tag: str = "") -> str:
    """List all blog posts, optionally filtered by tag."""
    posts = glob.glob(f"{BLOG_DIR}/**/*.mdx", recursive=True)
    results = []
    for post_path in sorted(posts, reverse=True):
        with open(post_path) as f:
            content = f.read()
            if content.startswith("---"):
                fm_end = content.index("---", 3)
                frontmatter = yaml.safe_load(content[3:fm_end])
                if tag and tag not in frontmatter.get("tags", []):
                    continue
                results.append(
                    f"- {frontmatter['title']} ({frontmatter['date']})"
                )
    return "\n".join(results[:20])

@mcp.tool()
def create_post(title: str, tags: str, content: str) -> str:
    """Create a new blog post."""
    # Implementation...
    return "Post created successfully"

7-2. Stock/Crypto Price MCP

Query real-time prices and analyze portfolios.

7-3. Calendar/Schedule MCP

Integrate with Google Calendar or Apple Calendar to manage schedules.

7-4. CI/CD Pipeline Monitoring MCP

Monitor GitHub Actions, Jenkins, CircleCI pipeline status.

7-5. Kubernetes Log Analysis MCP

Wrap kubectl to analyze Pod logs and detect anomalies.

7-6. Figma Design Extraction MCP

Extract design components via Figma API and convert to code.

7-7. Translation MCP

Provide multilingual translation via DeepL or Google Translate API.

7-8. Personal Notes/Wiki MCP

Let AI navigate and edit Obsidian or Logseq vaults.

7-9. Healthcare Data MCP

Analyze Apple Health or Fitbit data.

7-10. Smart Home IoT Control MCP

Control lights, temperature, and security systems via Home Assistant API.

from fastmcp import FastMCP
import httpx

mcp = FastMCP("Smart Home")

HA_URL = "http://homeassistant.local:8123"
HA_TOKEN = "your_long_lived_token"

@mcp.tool()
def control_light(entity_id: str, action: str) -> str:
    """Control a smart light.

    Args:
        entity_id: Home Assistant entity ID (e.g., light.living_room)
        action: 'on' or 'off'
    """
    service = "turn_on" if action == "on" else "turn_off"
    response = httpx.post(
        f"{HA_URL}/api/services/light/{service}",
        headers={"Authorization": f"Bearer {HA_TOKEN}"},
        json={"entity_id": entity_id},
    )
    return f"Light {entity_id}: {action} (status: {response.status_code})"

@mcp.tool()
def get_temperature(entity_id: str) -> str:
    """Get temperature from a sensor."""
    response = httpx.get(
        f"{HA_URL}/api/states/{entity_id}",
        headers={"Authorization": f"Bearer {HA_TOKEN}"},
    )
    data = response.json()
    return f"Temperature: {data['state']}{data['attributes'].get('unit_of_measurement', '')}"

8. MCP Security Best Practices

MCP servers grant AI access to your systems, making security critically important.

8-1. Input Validation

Python (Pydantic):

from pydantic import BaseModel, Field, validator

class QueryInput(BaseModel):
    sql: str = Field(..., max_length=5000)
    timeout: int = Field(default=30, ge=1, le=300)

    @validator("sql")
    def validate_sql(cls, v):
        forbidden = ["DROP", "DELETE", "TRUNCATE", "ALTER"]
        upper = v.upper()
        for keyword in forbidden:
            if keyword in upper:
                raise ValueError(f"Forbidden SQL keyword: {keyword}")
        return v

TypeScript (Zod):

const querySchema = z.object({
  sql: z
    .string()
    .max(5000)
    .refine(
      (sql) => {
        const forbidden = ['DROP', 'DELETE', 'TRUNCATE', 'ALTER']
        const upper = sql.toUpperCase()
        return !forbidden.some((kw) => upper.includes(kw))
      },
      { message: 'Forbidden SQL keyword detected' }
    ),
  timeout: z.number().min(1).max(300).default(30),
})

8-2. Principle of Least Privilege

  • DB servers: Use read-only accounts (SELECT only)
  • File servers: Restrict allowed directories, resolve symlinks
  • API servers: Use tokens with only required scopes
  • Network: Block internal network access

8-3. Rate Limiting

import time
from collections import defaultdict

class RateLimiter:
    def __init__(self, max_calls: int = 10, window: int = 60):
        self.max_calls = max_calls
        self.window = window
        self.calls = defaultdict(list)

    def check(self, key: str) -> bool:
        now = time.time()
        self.calls[key] = [
            t for t in self.calls[key] if now - t < self.window
        ]
        if len(self.calls[key]) >= self.max_calls:
            return False
        self.calls[key].append(now)
        return True

limiter = RateLimiter(max_calls=20, window=60)

@mcp.tool()
def rate_limited_tool(param: str) -> str:
    if not limiter.check("default"):
        return "Rate limit exceeded. Please wait before trying again."
    return do_work(param)

8-4. Secret Management

import os

# Load secrets from environment variables (never hardcode)
DB_PASSWORD = os.environ.get("DB_PASSWORD")
API_KEY = os.environ.get("API_KEY")

if not DB_PASSWORD or not API_KEY:
    raise RuntimeError("Required environment variables not set")

Pass environment variables in Claude Desktop config:

{
  "mcpServers": {
    "my-server": {
      "command": "python",
      "args": ["server.py"],
      "env": {
        "DB_PASSWORD": "secret_from_keychain",
        "API_KEY": "sk-xxx"
      }
    }
  }
}

8-5. Audit Logging

import json
import sys
from datetime import datetime

def audit_log(tool_name: str, params: dict, result: str, success: bool):
    log_entry = {
        "timestamp": datetime.utcnow().isoformat(),
        "tool": tool_name,
        "params": params,
        "success": success,
        "result_length": len(result),
    }
    print(json.dumps(log_entry), file=sys.stderr)

8-6. Sandboxing

Use Docker or virtual environments to isolate MCP servers:

# docker-compose.yml
services:
  mcp-server:
    build: .
    read_only: true
    security_opt:
      - no-new-privileges:true
    tmpfs:
      - /tmp:size=100M
    mem_limit: 512m
    cpus: '0.5'
    networks:
      - mcp-net
    volumes:
      - ./allowed-data:/data:ro

networks:
  mcp-net:
    driver: bridge
    internal: true # Block external internet access

9. The Future of MCP: 2026 Outlook

9-1. Agent-to-Agent MCP Communication

Currently MCP focuses mainly on human(user)-AI tool connections. In 2026, scenarios where AI agents communicate directly with each other via MCP will expand.

For example:

  • A code review agent asks a security analysis agent to scan for vulnerabilities via MCP
  • A data pipeline agent asks a monitoring agent to check for anomalies
  • A customer support agent queries an order management agent for order status

9-2. Multimodal MCP

Current MCP is text-centric, but multimodal extensions for images, audio, and video are underway.

  • Image resources: Deliver screenshots, charts, and design files directly to AI
  • Audio processing: AI analyzes meeting recordings
  • Video analysis: AI monitors security camera footage

9-3. MCP Registry (Central Server Registry)

Like npm or PyPI, an official registry for searching and installing MCP servers is being developed. Smithery currently serves this role, but a more standardized registry is forthcoming.

9-4. Enterprise MCP Gateway

Enterprise environments need a central gateway for managing MCP servers:

  • Access control: Who can use which MCP servers
  • Audit logs: Record all tool invocations
  • Cost management: Rate limiting API calls
  • Security policies: Control access to sensitive data

9-5. Linux Foundation AAIF Governance

In late 2025, MCP was donated to the Linux Foundation's AI and AI Foundation (AAIF). This means MCP governance is transitioning from a single company (Anthropic) to an open community.

Expected benefits:

  • More transparent spec development process
  • Increased participation from diverse companies
  • Strengthened interoperability standards
  • Possible certification programs

10. Quiz

Let's test what you have learned.

Q1. What is MCP's underlying protocol, and what are the three message types?

Answer: MCP is based on JSON-RPC 2.0. The three message types are:

  1. Request: Has an id field, expects a response
  2. Response: Returns results for a request
  3. Notification: No id field, one-way message
Q2. What are MCP's two transport methods and their primary use cases?

Answer:

  1. stdio: Communicates via stdin/stdout of local processes. Used when running local servers from Claude Desktop or Claude Code.
  2. Streamable HTTP: Communicates via HTTP POST and SSE. Used for remote server deployment and multi-tenant environments.
Q3. How do the three core primitives (Resources, Tools, Prompts) differ?

Answer:

  • Resources: Read-only data via URIs. Used by applications as context. (User-controlled)
  • Tools: Executable functions invoked by AI models. Parameters defined by JSON Schema. May have side effects. (Model-controlled)
  • Prompts: Pre-defined reusable prompt templates. (User-controlled)
Q4. What is the simplest way to define an MCP tool in FastMCP?

Answer: Use the @mcp.tool() decorator. The function's docstring becomes the tool description, and type hints are automatically converted to JSON Schema.

from fastmcp import FastMCP
mcp = FastMCP("My Server")

@mcp.tool()
def greet(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"
Q5. What are the 5 most important MCP server security principles?

Answer:

  1. Input validation: Validate all parameters with Zod/Pydantic
  2. Least privilege: Grant only necessary permissions (read-only DB accounts, etc.)
  3. Rate limiting: Limit API call frequency
  4. Secret management: Use environment variables, never hardcode
  5. Audit logging: Record all tool invocations

Additionally, sandboxing (Docker isolation) and OAuth 2.0 authentication are important.


11. References

  1. MCP Official Specification - Full MCP spec
  2. MCP Official Documentation - Getting started guide and tutorials
  3. MCP GitHub Repository - SDKs and reference servers
  4. FastMCP Documentation - Python FastMCP framework
  5. MCP TypeScript SDK - TypeScript SDK
  6. MCP Python SDK - Python SDK
  7. Awesome MCP Servers - Community MCP server list
  8. Smithery - MCP server registry
  9. MCP Inspector - MCP server testing tool
  10. Claude Desktop MCP Setup Guide - Claude Desktop integration
  11. Claude Code MCP Setup - Claude Code integration
  12. Grafana MCP Server - Official Grafana MCP server
  13. GitHub MCP Server - Official GitHub MCP server
  14. PostgreSQL MCP Server - PostgreSQL MCP
  15. Playwright MCP Server - Microsoft Playwright MCP
  16. Slack MCP Server - Slack MCP
  17. Filesystem MCP Server - Filesystem MCP
  18. Cloudflare MCP Workers - Cloudflare deployment guide
  19. MCP OAuth 2.0 Specification - Authentication spec
  20. Linux Foundation AAIF - MCP governance
  21. Anthropic MCP Blog - MCP announcement
  22. OpenAI MCP Support - OpenAI adopts MCP
  23. Google MCP Support - Google supports MCP