diff --git a/servers/claude/Dockerfile b/servers/claude/Dockerfile new file mode 100644 index 0000000..69bd53c --- /dev/null +++ b/servers/claude/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY server.py . + +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ + CMD python -c "import httpx; httpx.get('http://localhost:8000/health')" || exit 1 + +CMD ["fastmcp", "run", "server.py", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/servers/claude/README.md b/servers/claude/README.md new file mode 100644 index 0000000..6798a84 --- /dev/null +++ b/servers/claude/README.md @@ -0,0 +1,343 @@ +# Anthropic Claude MCP Server + +MCP server providing comprehensive access to Anthropic's Claude API. + +## Features + +### Core Capabilities +- **Messages API** - Full access to Claude 3.5 Sonnet, Haiku, and Claude 3 Opus +- **Simple Chat** - Easy-to-use chat interface +- **Vision** - Analyze images via URL or base64 +- **Multi-turn Conversations** - Maintain conversation history +- **Token Counting** - Estimate token usage before making calls +- **Model Comparison** - Compare responses across different Claude models +- **Structured Extraction** - Extract structured data from unstructured text +- **Model Information** - Get details about Claude models and capabilities + +## Installation + +1. Clone this repository +2. Install dependencies: +```bash +pip install -r requirements.txt +``` + +3. Set up your environment variables: +```bash +cp .env.example .env +# Edit .env and add your Anthropic API key +``` + +## Configuration + +### Get Your Anthropic API Key +1. Go to https://console.anthropic.com/settings/keys +2. Sign in or create an account +3. Click "Create Key" +4. Copy the key and add it to your `.env` file + +### Claude Desktop Configuration + +Add to your `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "claude": { + "command": "fastmcp", + "args": ["run", "server.py"], + "env": { + "ANTHROPIC_API_KEY": "your_api_key_here" + } + } + } +} +``` + +## Available Tools + +### chat +Simple chat interface - send a prompt, get a response. + +**Parameters:** +- `prompt` (required): Your message or question +- `model`: Claude model (default: "claude-3-5-sonnet-20241022") +- `max_tokens`: Maximum response length (default: 4096) +- `system`: Optional system prompt for context +- `temperature`: Creativity level 0-1 + +**Example:** +```json +{ + "prompt": "Explain quantum computing in simple terms", + "max_tokens": 500 +} +``` + +### create_message +Full Messages API with all parameters and features. + +**Parameters:** +- `messages` (required): Conversation history +- `model`: Claude model +- `max_tokens` (required): Max response length +- `system`: System prompt +- `temperature`, `top_p`, `top_k`: Sampling parameters +- `stop_sequences`: Custom stop sequences + +### analyze_image +Analyze images using Claude's vision capabilities. + +**Parameters:** +- `image_url` (required): URL of image to analyze +- `prompt`: Question about the image (default: "What's in this image?") +- `model`: Vision-capable model +- `max_tokens`: Max response length + +**Example:** +```json +{ + "image_url": "https://example.com/photo.jpg", + "prompt": "Describe the architectural style of this building" +} +``` + +### analyze_image_base64 +Analyze base64-encoded images. + +**Parameters:** +- `image_base64` (required): Base64 image data +- `media_type` (required): image/jpeg, image/png, image/gif, or image/webp +- `prompt`: Question about the image +- `model`: Vision model +- `max_tokens`: Max response length + +### multi_turn_conversation +Continue a conversation with context from previous messages. + +**Parameters:** +- `conversation_history` (required): Previous messages +- `new_message` (required): New user message +- `model`: Claude model +- `max_tokens`: Max response length +- `system`: Optional system prompt + +**Returns:** Response plus updated conversation history + +**Example:** +```json +{ + "conversation_history": [ + {"role": "user", "content": "What's the capital of France?"}, + {"role": "assistant", "content": "The capital of France is Paris."} + ], + "new_message": "What's the population?" +} +``` + +### count_tokens +Estimate token usage for messages. + +**Parameters:** +- `messages` (required): Messages to count tokens for +- `model`: Model (affects tokenization) +- `system`: Optional system prompt + +**Returns:** Input and output token counts + +### get_model_info +Get information about Claude models. + +**Parameters:** +- `model`: Model identifier (default: "claude-3-5-sonnet-20241022") + +**Returns:** Context window, capabilities, tier, description + +### compare_responses +Get responses from multiple models for comparison. + +**Parameters:** +- `prompt` (required): Question or task +- `models`: List of models to compare (max 3, default: Sonnet and Haiku) +- `max_tokens`: Max tokens per response +- `system`: Optional system prompt + +**Returns:** Responses from each model with usage stats + +**Example:** +```json +{ + "prompt": "Write a haiku about programming", + "models": ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"] +} +``` + +### extract_structured_data +Extract structured data from unstructured text. + +**Parameters:** +- `text` (required): Unstructured text +- `schema_description` (required): Description of desired structure +- `model`: Claude model +- `max_tokens`: Max response length + +**Example:** +```json +{ + "text": "Contact John Smith at john@example.com or call 555-1234", + "schema_description": "Extract as JSON with fields: name, email, phone" +} +``` + +## Claude Models + +### Claude 3.5 Family +- **claude-3-5-sonnet-20241022**: Most intelligent, best overall performance +- **claude-3-5-haiku-20241022**: Fastest model, cost-effective + +### Claude 3 Family +- **claude-3-opus-20240229**: Top-tier performance for complex tasks +- **claude-3-sonnet-20240229**: Balanced intelligence and speed +- **claude-3-haiku-20240307**: Fast and economical + +All models support: +- 200K context window +- Vision capabilities +- Tool use +- Multi-turn conversations + +## Usage Examples + +### Simple Question +```json +{ + "tool": "chat", + "prompt": "What are the three laws of robotics?" +} +``` + +### Creative Writing with System Prompt +```json +{ + "tool": "chat", + "prompt": "Write a short story about a time traveler", + "system": "You are a creative writer specializing in science fiction", + "temperature": 0.8, + "max_tokens": 1000 +} +``` + +### Image Analysis +```json +{ + "tool": "analyze_image", + "image_url": "https://example.com/diagram.png", + "prompt": "Explain what this technical diagram shows" +} +``` + +### Ongoing Conversation +```json +{ + "tool": "multi_turn_conversation", + "conversation_history": [ + {"role": "user", "content": "I'm planning a trip to Japan"}, + {"role": "assistant", "content": "That sounds exciting! How long will you be staying?"} + ], + "new_message": "Two weeks in spring" +} +``` + +### Model Comparison +```json +{ + "tool": "compare_responses", + "prompt": "Explain the theory of relativity", + "models": ["claude-3-5-sonnet-20241022", "claude-3-opus-20240229"] +} +``` + +### Data Extraction +```json +{ + "tool": "extract_structured_data", + "text": "Apple Inc. reported revenue of $394.3B in fiscal 2024, up 2% YoY", + "schema_description": "Extract as JSON: company, revenue, year, growth_rate" +} +``` + +## Error Handling + +The server includes comprehensive error handling for: +- Invalid API keys +- Rate limiting (429 errors) +- Token limit exceeded (400 errors) +- Invalid model names +- Malformed requests +- Network timeouts + +## Rate Limits + +Anthropic enforces rate limits based on your account tier: +- Free tier: Limited requests per day +- Build tier: Higher daily limits +- Scale tier: Enterprise-level limits + +Check your limits at: https://console.anthropic.com/settings/limits + +## Pricing + +Approximate costs per million tokens (input/output): +- **Claude 3.5 Sonnet**: $3 / $15 +- **Claude 3.5 Haiku**: $0.80 / $4 +- **Claude 3 Opus**: $15 / $75 +- **Claude 3 Sonnet**: $3 / $15 +- **Claude 3 Haiku**: $0.25 / $1.25 + +Current pricing: https://www.anthropic.com/pricing + +## Best Practices + +1. **Use System Prompts**: Set context and instructions for better responses +2. **Manage Context**: Keep conversation history relevant, trim old messages +3. **Choose the Right Model**: Haiku for speed, Sonnet for balance, Opus for complexity +4. **Token Management**: Use count_tokens to estimate costs before calls +5. **Error Handling**: Implement retry logic for rate limits +6. **Vision**: Images count toward token limits (analyze_image tools) + +## Security Notes + +- Never commit your API key to version control +- Use environment variables for sensitive data +- Rotate API keys regularly +- Monitor usage in Anthropic Console +- Set spending limits in your account + +## Troubleshooting + +### "Invalid API Key" Error +- Verify key is correct in `.env` file +- Check key is active at https://console.anthropic.com/settings/keys +- Ensure no extra spaces or quotes + +### Rate Limit Errors (429) +- Implement exponential backoff +- Upgrade account tier if needed +- Monitor usage at console.anthropic.com + +### Context Length Errors +- All Claude models support 200K tokens +- Count tokens before sending +- Trim old messages from conversation history + +## Resources + +- [Anthropic Documentation](https://docs.anthropic.com/) +- [Messages API Reference](https://docs.anthropic.com/en/api/messages) +- [Claude Models](https://docs.anthropic.com/en/docs/about-claude/models) +- [Pricing](https://www.anthropic.com/pricing) +- [Model Context Protocol](https://modelcontextprotocol.io) + +## License + +MIT License - feel free to use in your projects! \ No newline at end of file diff --git a/servers/claude/requirements.txt b/servers/claude/requirements.txt new file mode 100644 index 0000000..2ab610c --- /dev/null +++ b/servers/claude/requirements.txt @@ -0,0 +1,4 @@ +fastmcp>=0.2.0 +httpx>=0.27.0 +python-dotenv>=1.0.0 +uvicorn>=0.30.0 \ No newline at end of file diff --git a/servers/claude/server.json b/servers/claude/server.json new file mode 100644 index 0000000..167f650 --- /dev/null +++ b/servers/claude/server.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://registry.nimbletools.ai/schemas/2025-09-22/nimbletools-server.schema.json", + "name": "ai.nimbletools/claude", + "version": "1.0.0", + "description": "Anthropic Claude API: messages, vision, multi-turn chat, token counting, and model comparison", + "status": "active", + "repository": { + "url": "https://github.com/NimbleBrainInc/mcp-claude", + "source": "github", + "branch": "main" + }, + "websiteUrl": "https://www.anthropic.com/", + "packages": [ + { + "registryType": "oci", + "registryBaseUrl": "https://docker.io", + "identifier": "nimbletools/mcp-claude", + "version": "1.0.0", + "transport": { + "type": "streamable-http", + "url": "https://mcp.nimbletools.ai/mcp" + }, + "environmentVariables": [ + { + "name": "ANTHROPIC_API_KEY", + "description": "Anthropic API key for accessing Claude models (get key at console.anthropic.com/settings/keys)", + "isRequired": true, + "isSecret": true, + "example": "sk-ant-..." + } + ] + } + ], + "_meta": { + "ai.nimbletools.mcp/v1": { + "container": { + "healthCheck": { + "path": "/health", + "port": 8000 + } + }, + "capabilities": { + "tools": true, + "resources": false, + "prompts": false + }, + "resources": { + "limits": { + "memory": "512Mi", + "cpu": "500m" + }, + "requests": { + "memory": "256Mi", + "cpu": "100m" + } + }, + "deployment": { + "protocol": "http", + "port": 8000, + "mcpPath": "/mcp" + }, + "display": { + "name": "Claude", + "category": "ai-ml", + "tags": [ + "anthropic", + "claude", + "claude-3", + "claude-35", + "chat", + "vision", + "conversation", + "requires-api-key" + ], + "branding": { + "logoUrl": "https://static.nimbletools.ai/logos/claude.png", + "iconUrl": "https://static.nimbletools.ai/icons/claude.png" + }, + "documentation": { + "readmeUrl": "https://raw.githubusercontent.com/NimbleBrainInc/mcp-claude/main/README.md" + } + } + } + } +} \ No newline at end of file diff --git a/servers/claude/server.py b/servers/claude/server.py new file mode 100644 index 0000000..99f29c0 --- /dev/null +++ b/servers/claude/server.py @@ -0,0 +1,521 @@ +""" +Anthropic Claude MCP Server +Provides access to Claude API capabilities including messages, streaming, +vision, system prompts, and token counting. +""" + +import os +from typing import Optional, List, Dict, Any +import httpx +from fastmcp import FastMCP +import json + +# Initialize FastMCP server +mcp = FastMCP("Anthropic Claude API") + +# Claude API configuration +ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") +ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1" +ANTHROPIC_VERSION = "2023-06-01" + +if not ANTHROPIC_API_KEY: + raise ValueError("ANTHROPIC_API_KEY environment variable is required") + + +async def make_claude_request( + method: str, + endpoint: str, + data: Optional[Dict[str, Any]] = None +) -> Any: + """Make a request to Claude API""" + headers = { + "x-api-key": ANTHROPIC_API_KEY, + "anthropic-version": ANTHROPIC_VERSION, + "content-type": "application/json" + } + + url = f"{ANTHROPIC_BASE_URL}/{endpoint}" + + async with httpx.AsyncClient(timeout=300.0) as client: + if method == "GET": + response = await client.get(url, headers=headers) + elif method == "POST": + response = await client.post(url, headers=headers, json=data) + else: + raise ValueError(f"Unsupported HTTP method: {method}") + + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def create_message( + messages: List[Dict[str, Any]], + model: str = "claude-3-5-sonnet-20241022", + max_tokens: int = 4096, + system: Optional[str] = None, + temperature: Optional[float] = None, + top_p: Optional[float] = None, + top_k: Optional[int] = None, + stop_sequences: Optional[List[str]] = None +) -> Dict[str, Any]: + """ + Create a message with Claude. + + Args: + messages: List of message objects with 'role' and 'content' + Example: [{"role": "user", "content": "Hello!"}] + model: Claude model to use (claude-3-5-sonnet-20241022, claude-3-5-haiku-20241022, + claude-3-opus-20240229, claude-3-sonnet-20240229, claude-3-haiku-20240307) + max_tokens: Maximum tokens to generate (required, up to 8192) + system: System prompt to set context and instructions + temperature: Sampling temperature 0-1 (default: 1.0) + top_p: Nucleus sampling threshold (default: none) + top_k: Top-k sampling (default: none) + stop_sequences: Custom stop sequences + + Returns: + Complete message response with content, usage, and metadata + """ + data = { + "model": model, + "messages": messages, + "max_tokens": max_tokens + } + + if system: + data["system"] = system + if temperature is not None: + data["temperature"] = temperature + if top_p is not None: + data["top_p"] = top_p + if top_k is not None: + data["top_k"] = top_k + if stop_sequences: + data["stop_sequences"] = stop_sequences + + result = await make_claude_request("POST", "messages", data) + return result + + +@mcp.tool() +async def chat( + prompt: str, + model: str = "claude-3-5-sonnet-20241022", + max_tokens: int = 4096, + system: Optional[str] = None, + temperature: Optional[float] = None +) -> str: + """ + Simple chat interface - send a prompt and get a text response. + + Args: + prompt: Your message or question + model: Claude model to use + max_tokens: Maximum tokens to generate + system: Optional system prompt for context + temperature: Sampling temperature 0-1 + + Returns: + Claude's text response + """ + messages = [{"role": "user", "content": prompt}] + + data = { + "model": model, + "messages": messages, + "max_tokens": max_tokens + } + + if system: + data["system"] = system + if temperature is not None: + data["temperature"] = temperature + + result = await make_claude_request("POST", "messages", data) + + # Extract text from content blocks + text_content = [] + for block in result.get("content", []): + if block.get("type") == "text": + text_content.append(block.get("text", "")) + + return "\n".join(text_content) + + +@mcp.tool() +async def analyze_image( + image_url: str, + prompt: str = "What's in this image?", + model: str = "claude-3-5-sonnet-20241022", + max_tokens: int = 4096 +) -> str: + """ + Analyze an image using Claude's vision capabilities. + + Args: + image_url: URL of the image to analyze + prompt: Question or instruction about the image + model: Vision-capable Claude model + max_tokens: Maximum tokens in response + + Returns: + Analysis of the image + """ + messages = [ + { + "role": "user", + "content": [ + { + "type": "image", + "source": { + "type": "url", + "url": image_url + } + }, + { + "type": "text", + "text": prompt + } + ] + } + ] + + data = { + "model": model, + "messages": messages, + "max_tokens": max_tokens + } + + result = await make_claude_request("POST", "messages", data) + + # Extract text from content blocks + text_content = [] + for block in result.get("content", []): + if block.get("type") == "text": + text_content.append(block.get("text", "")) + + return "\n".join(text_content) + + +@mcp.tool() +async def analyze_image_base64( + image_base64: str, + media_type: str, + prompt: str = "What's in this image?", + model: str = "claude-3-5-sonnet-20241022", + max_tokens: int = 4096 +) -> str: + """ + Analyze a base64-encoded image using Claude's vision capabilities. + + Args: + image_base64: Base64 encoded image data + media_type: Image media type (image/jpeg, image/png, image/gif, image/webp) + prompt: Question or instruction about the image + model: Vision-capable Claude model + max_tokens: Maximum tokens in response + + Returns: + Analysis of the image + """ + messages = [ + { + "role": "user", + "content": [ + { + "type": "image", + "source": { + "type": "base64", + "media_type": media_type, + "data": image_base64 + } + }, + { + "type": "text", + "text": prompt + } + ] + } + ] + + data = { + "model": model, + "messages": messages, + "max_tokens": max_tokens + } + + result = await make_claude_request("POST", "messages", data) + + # Extract text from content blocks + text_content = [] + for block in result.get("content", []): + if block.get("type") == "text": + text_content.append(block.get("text", "")) + + return "\n".join(text_content) + + +@mcp.tool() +async def count_tokens( + messages: List[Dict[str, Any]], + model: str = "claude-3-5-sonnet-20241022", + system: Optional[str] = None +) -> Dict[str, int]: + """ + Count tokens for a given set of messages without making an API call. + Uses the Messages API with a special parameter to only count tokens. + + Args: + messages: List of message objects to count tokens for + model: Claude model (affects tokenization) + system: Optional system prompt to include in count + + Returns: + Dictionary with input_tokens count + """ + # Note: Anthropic's API doesn't have a dedicated token counting endpoint + # This is a workaround that makes a message request with max_tokens=1 + # to get token count from the usage field + data = { + "model": model, + "messages": messages, + "max_tokens": 1 + } + + if system: + data["system"] = system + + result = await make_claude_request("POST", "messages", data) + + return { + "input_tokens": result.get("usage", {}).get("input_tokens", 0), + "output_tokens": result.get("usage", {}).get("output_tokens", 0) + } + + +@mcp.tool() +async def multi_turn_conversation( + conversation_history: List[Dict[str, str]], + new_message: str, + model: str = "claude-3-5-sonnet-20241022", + max_tokens: int = 4096, + system: Optional[str] = None +) -> Dict[str, Any]: + """ + Continue a multi-turn conversation by adding a new message. + + Args: + conversation_history: Previous messages in the conversation + Example: [{"role": "user", "content": "Hi"}, + {"role": "assistant", "content": "Hello!"}] + new_message: New user message to add to the conversation + model: Claude model to use + max_tokens: Maximum tokens to generate + system: Optional system prompt + + Returns: + Complete response with new assistant message and full conversation + """ + # Add new user message to history + messages = conversation_history + [{"role": "user", "content": new_message}] + + data = { + "model": model, + "messages": messages, + "max_tokens": max_tokens + } + + if system: + data["system"] = system + + result = await make_claude_request("POST", "messages", data) + + # Extract assistant's response + text_content = [] + for block in result.get("content", []): + if block.get("type") == "text": + text_content.append(block.get("text", "")) + + assistant_message = "\n".join(text_content) + + return { + "response": assistant_message, + "updated_history": messages + [{"role": "assistant", "content": assistant_message}], + "usage": result.get("usage", {}), + "model": result.get("model", model), + "stop_reason": result.get("stop_reason", "") + } + + +@mcp.tool() +async def get_model_info(model: str = "claude-3-5-sonnet-20241022") -> Dict[str, Any]: + """ + Get information about a Claude model including context window and capabilities. + + Args: + model: Model identifier + + Returns: + Model information including context window, capabilities, and pricing tier + """ + # Static model information (as of API version 2023-06-01) + model_info = { + "claude-3-5-sonnet-20241022": { + "name": "Claude 3.5 Sonnet", + "version": "20241022", + "context_window": 200000, + "max_output": 8192, + "supports_vision": True, + "supports_tool_use": True, + "tier": "intelligent", + "description": "Our most intelligent model with best-in-class performance" + }, + "claude-3-5-haiku-20241022": { + "name": "Claude 3.5 Haiku", + "version": "20241022", + "context_window": 200000, + "max_output": 8192, + "supports_vision": True, + "supports_tool_use": True, + "tier": "fast", + "description": "Our fastest model for quick, intelligent responses" + }, + "claude-3-opus-20240229": { + "name": "Claude 3 Opus", + "version": "20240229", + "context_window": 200000, + "max_output": 4096, + "supports_vision": True, + "supports_tool_use": True, + "tier": "powerful", + "description": "Top-level performance for complex tasks" + }, + "claude-3-sonnet-20240229": { + "name": "Claude 3 Sonnet", + "version": "20240229", + "context_window": 200000, + "max_output": 4096, + "supports_vision": True, + "supports_tool_use": True, + "tier": "balanced", + "description": "Balance of intelligence and speed" + }, + "claude-3-haiku-20240307": { + "name": "Claude 3 Haiku", + "version": "20240307", + "context_window": 200000, + "max_output": 4096, + "supports_vision": True, + "supports_tool_use": True, + "tier": "fast", + "description": "Fast and cost-effective" + } + } + + info = model_info.get(model, { + "name": model, + "context_window": "unknown", + "supports_vision": "unknown", + "description": "Model information not available" + }) + + info["model_id"] = model + return info + + +@mcp.tool() +async def compare_responses( + prompt: str, + models: List[str] = ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"], + max_tokens: int = 1024, + system: Optional[str] = None +) -> Dict[str, Any]: + """ + Get responses from multiple Claude models for comparison. + + Args: + prompt: Question or task to send to all models + models: List of model identifiers to compare (max 3 for cost reasons) + max_tokens: Maximum tokens per response + system: Optional system prompt + + Returns: + Dictionary with responses from each model and timing information + """ + if len(models) > 3: + raise ValueError("Maximum 3 models allowed for comparison") + + messages = [{"role": "user", "content": prompt}] + + results = {} + for model in models: + data = { + "model": model, + "messages": messages, + "max_tokens": max_tokens + } + + if system: + data["system"] = system + + result = await make_claude_request("POST", "messages", data) + + # Extract text response + text_content = [] + for block in result.get("content", []): + if block.get("type") == "text": + text_content.append(block.get("text", "")) + + results[model] = { + "response": "\n".join(text_content), + "usage": result.get("usage", {}), + "stop_reason": result.get("stop_reason", "") + } + + return results + + +@mcp.tool() +async def extract_structured_data( + text: str, + schema_description: str, + model: str = "claude-3-5-sonnet-20241022", + max_tokens: int = 4096 +) -> str: + """ + Extract structured data from unstructured text based on a schema description. + + Args: + text: Unstructured text to extract data from + schema_description: Description of the data structure you want (e.g., + "Extract person's name, email, and phone number as JSON") + model: Claude model to use + max_tokens: Maximum tokens for response + + Returns: + Extracted structured data (typically as JSON string) + """ + system_prompt = f"""Extract structured data according to this schema: +{schema_description} + +Return ONLY the structured data, no additional explanation.""" + + messages = [{"role": "user", "content": text}] + + data = { + "model": model, + "messages": messages, + "max_tokens": max_tokens, + "system": system_prompt + } + + result = await make_claude_request("POST", "messages", data) + + # Extract text from content blocks + text_content = [] + for block in result.get("content", []): + if block.get("type") == "text": + text_content.append(block.get("text", "")) + + return "\n".join(text_content) \ No newline at end of file diff --git a/servers/claude/test.json b/servers/claude/test.json new file mode 100644 index 0000000..7f8d31f --- /dev/null +++ b/servers/claude/test.json @@ -0,0 +1,30 @@ +{ + "environment": { + "ANTHROPIC_API_KEY": "${ANTHROPIC_API_KEY}" + }, + "tests": [ + { + "name": "Test simple chat", + "tool": "chat", + "arguments": { + "prompt": "Say 'test successful' and nothing else", + "max_tokens": 20 + }, + "expect": { + "type": "text", + "contains": "test successful" + } + }, + { + "name": "Test get model info", + "tool": "get_model_info", + "arguments": { + "model": "claude-3-5-sonnet-20241022" + }, + "expect": { + "type": "object", + "hasKeys": ["name", "context_window", "supports_vision"] + } + } + ] +} \ No newline at end of file diff --git a/servers/openai/Dockerfile b/servers/openai/Dockerfile new file mode 100644 index 0000000..5f8462d --- /dev/null +++ b/servers/openai/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Copy requirements and install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy server code +COPY server.py . + +# Expose the HTTP port +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ + CMD python -c "import httpx; httpx.get('http://localhost:8000/health')" || exit 1 + +# Run the server +CMD ["fastmcp", "run", "server.py", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/servers/openai/README.md b/servers/openai/README.md new file mode 100644 index 0000000..206d703 --- /dev/null +++ b/servers/openai/README.md @@ -0,0 +1,308 @@ +# OpenAI MCP Server + +MCP server providing comprehensive access to OpenAI's API capabilities. + +## Features + +### Core Capabilities +- **Chat Completions** - Generate responses using GPT-4o, GPT-4, and GPT-3.5 models +- **Embeddings** - Create vector embeddings for semantic search and similarity +- **Image Generation** - Generate images with DALL-E 3 and DALL-E 2 +- **Text-to-Speech** - Convert text to natural-sounding speech +- **Speech-to-Text** - Transcribe audio using Whisper +- **Vision Analysis** - Analyze images with GPT-4 Vision +- **Content Moderation** - Check content against usage policies +- **Model Management** - List and explore available models + +## Installation + +1. Clone this repository +2. Install dependencies: +```bash +pip install -r requirements.txt +``` + +3. Set up your environment variables: +```bash +cp .env.example .env +# Edit .env and add your OpenAI API key +``` + +## Configuration + +### Get Your OpenAI API Key +1. Go to https://platform.openai.com/api-keys +2. Sign in or create an account +3. Click "Create new secret key" +4. Copy the key and add it to your `.env` file + +## Running the Server + +### HTTP Mode (Recommended for NimbleBrain) + +Start the server: +```bash +# Set your API key +export OPENAI_API_KEY=your_api_key_here + +# Run the server (default port 8000) +fastmcp run openai_server.py + +# Or specify a custom port +fastmcp run openai_server.py --port 8080 +``` + +The server will be available at `http://localhost:8000` + +### Claude Desktop Configuration + +Add to your `claude_desktop_config.json`: + +**HTTP Configuration:** +```json +{ + "mcpServers": { + "openai": { + "url": "http://localhost:8000" + } + } +} +``` + +**Alternative - Direct Python (stdio):** + +If you need stdio mode instead of HTTP, you can run directly: + +**Windows** (`%APPDATA%\Claude\claude_desktop_config.json`): +```json +{ + "mcpServers": { + "openai": { + "command": "python", + "args": ["-m", "fastmcp", "run", "openai_server.py"], + "env": { + "OPENAI_API_KEY": "your_api_key_here" + } + } + } +} +``` + +**macOS** (`~/Library/Application Support/Claude/claude_desktop_config.json`): +```json +{ + "mcpServers": { + "openai": { + "command": "python3", + "args": ["-m", "fastmcp", "run", "openai_server.py"], + "env": { + "OPENAI_API_KEY": "your_api_key_here" + } + } + } +} +``` + +## Available Tools + +### chat_completion +Generate conversational responses using OpenAI's chat models. + +**Parameters:** +- `messages` (required): List of message objects with 'role' and 'content' +- `model`: Model name (default: "gpt-4o-mini") +- `temperature`: Creativity level 0-2 (default: 1.0) +- `max_tokens`: Maximum response length +- `response_format`: Optional "json_object" for JSON responses + +**Example:** +```python +messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Explain quantum computing in simple terms."} +] +``` + +### create_embedding +Generate vector embeddings for text. + +**Parameters:** +- `text` (required): Text to embed +- `model`: Embedding model (default: "text-embedding-3-small") + +**Use Cases:** +- Semantic search +- Document similarity +- Clustering and classification +- Recommendation systems + +### generate_image +Create images from text descriptions using DALL-E. + +**Parameters:** +- `prompt` (required): Description of desired image +- `model`: "dall-e-3" or "dall-e-2" (default: "dall-e-3") +- `size`: Image dimensions (1024x1024, 1792x1024, 1024x1792) +- `quality`: "standard" or "hd" (DALL-E 3 only) +- `n`: Number of images (1-10, only 1 for DALL-E 3) + +### text_to_speech +Convert text to natural-sounding audio. + +**Parameters:** +- `text` (required): Text to convert +- `voice`: alloy, echo, fable, onyx, nova, shimmer (default: "alloy") +- `model`: "tts-1" or "tts-1-hd" (default: "tts-1") +- `speed`: Speech rate 0.25-4.0 (default: 1.0) + +**Returns:** Base64 encoded MP3 audio + +### transcribe_audio +Transcribe audio to text using Whisper. + +**Parameters:** +- `audio_file_base64` (required): Base64 encoded audio file +- `model`: "whisper-1" +- `language`: Optional language code (auto-detected if not provided) +- `response_format`: json, text, srt, vtt, verbose_json + +### analyze_image +Analyze images using GPT-4 Vision. + +**Parameters:** +- `image_url` (required): URL of image to analyze +- `prompt`: Question about the image (default: "What's in this image?") +- `model`: Vision model (default: "gpt-4o-mini") +- `max_tokens`: Maximum response length + +### moderate_content +Check if content violates OpenAI's usage policies. + +**Parameters:** +- `text` (required): Content to moderate +- `model`: "text-moderation-latest" or "text-moderation-stable" + +**Returns:** Flags and scores for various content categories + +### list_models +Get all available OpenAI models with metadata. + +## Usage Examples + +### Chat Conversation +```json +{ + "messages": [ + {"role": "system", "content": "You are a creative writing assistant."}, + {"role": "user", "content": "Write a haiku about programming."} + ], + "model": "gpt-4o", + "temperature": 0.8 +} +``` + +### Generate Marketing Image +```json +{ + "prompt": "A modern minimalist logo for a tech startup, blue and white color scheme, professional", + "model": "dall-e-3", + "size": "1024x1024", + "quality": "hd" +} +``` + +### Create Product Description Embeddings +```json +{ + "text": "Wireless Bluetooth headphones with active noise cancellation and 30-hour battery life", + "model": "text-embedding-3-small" +} +``` + +### Transcribe Meeting Recording +```json +{ + "audio_file_base64": "", + "language": "en", + "response_format": "verbose_json" +} +``` + +## Model Recommendations + +### Chat Models +- **gpt-4o**: Best overall, multimodal, fast +- **gpt-4o-mini**: Cost-effective, very fast +- **gpt-4-turbo**: High intelligence, good for complex tasks +- **gpt-3.5-turbo**: Fast and affordable for simple tasks + +### Embedding Models +- **text-embedding-3-small**: Best price/performance (1536 dimensions) +- **text-embedding-3-large**: Highest quality (3072 dimensions) + +### Image Models +- **dall-e-3**: Higher quality, better prompt following +- **dall-e-2**: More images per request, lower cost + +## Error Handling + +The server includes comprehensive error handling for: +- Invalid API keys +- Rate limiting +- Invalid model names +- Malformed requests +- Network timeouts + +## Rate Limits + +OpenAI enforces rate limits based on your account tier: +- Free tier: Limited requests per minute +- Pay-as-you-go: Higher limits based on usage +- Enterprise: Custom limits + +Check your limits at: https://platform.openai.com/account/limits + +## Pricing + +Approximate costs (check OpenAI pricing page for current rates): +- **GPT-4o**: ~$2.50 per 1M input tokens +- **GPT-4o-mini**: ~$0.15 per 1M input tokens +- **Text Embeddings**: ~$0.02 per 1M tokens +- **DALL-E 3**: ~$0.04 per standard image +- **Whisper**: ~$0.006 per minute + +## Security Notes + +- Never commit your API key to version control +- Use environment variables for sensitive data +- Rotate API keys regularly +- Monitor usage on OpenAI dashboard +- Set spending limits in your OpenAI account + +## Troubleshooting + +### "Invalid API Key" Error +- Verify key is correct in `.env` file +- Ensure no extra spaces or quotes +- Check key is active at https://platform.openai.com/api-keys + +### Rate Limit Errors +- Implement exponential backoff +- Upgrade account tier if needed +- Reduce request frequency + +### Timeout Errors +- Increase timeout in httpx client +- Check network connectivity +- Try with smaller requests + +## Resources + +- [OpenAI API Documentation](https://platform.openai.com/docs) +- [OpenAI Pricing](https://openai.com/pricing) +- [OpenAI Cookbook](https://cookbook.openai.com/) +- [Model Context Protocol](https://modelcontextprotocol.io) + +## License + +MIT License - feel free to use in your projects! \ No newline at end of file diff --git a/servers/openai/requirements.txt b/servers/openai/requirements.txt new file mode 100644 index 0000000..2ab610c --- /dev/null +++ b/servers/openai/requirements.txt @@ -0,0 +1,4 @@ +fastmcp>=0.2.0 +httpx>=0.27.0 +python-dotenv>=1.0.0 +uvicorn>=0.30.0 \ No newline at end of file diff --git a/servers/openai/server.json b/servers/openai/server.json new file mode 100644 index 0000000..f1ed6c4 --- /dev/null +++ b/servers/openai/server.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://registry.nimbletools.ai/schemas/2025-09-22/nimbletools-server.schema.json", + "name": "ai.nimbletools/openai", + "version": "1.0.0", + "description": "OpenAI API integration: chat, embeddings, DALL-E, TTS, Whisper, vision, and moderation", + "icon": "https://openai.com/favicon.ico", + "homepage": "https://platform.openai.com/docs/api-reference", + "license": "MIT", + "keywords": [ + "openai", + "gpt", + "gpt-4", + "gpt-4o", + "chat", + "embeddings", + "dall-e", + "image-generation", + "tts", + "text-to-speech", + "whisper", + "transcription", + "vision", + "ai", + "llm" + ], + "categories": ["AI & LLM", "Data & Analytics"], + "packages": [ + { + "registryType": "oci", + "identifier": "nimbletools/openai", + "version": "1.0.0", + "transport": { + "type": "stdio" + } + } + ], + "_meta": { + "ai.nimbletools.mcp/v1": { + "container": { + "healthCheck": { + "path": "/health", + "port": 8000, + "interval": 30, + "timeout": 10, + "retries": 3 + } + }, + "resources": { + "limits": { + "memory": "512Mi", + "cpu": "500m" + }, + "requests": { + "memory": "256Mi", + "cpu": "100m" + } + }, + "secrets": [ + { + "name": "OPENAI_API_KEY", + "description": "OpenAI API key from https://platform.openai.com/api-keys", + "required": true + } + ] + } + } +} \ No newline at end of file diff --git a/servers/openai/server.py b/servers/openai/server.py new file mode 100644 index 0000000..1123289 --- /dev/null +++ b/servers/openai/server.py @@ -0,0 +1,339 @@ +""" +OpenAI MCP Server +Provides access to OpenAI API capabilities including chat completions, embeddings, +image generation, text-to-speech, speech-to-text, and model management. +""" + +import os +from typing import Optional, List, Dict, Any +import httpx +from fastmcp import FastMCP +import base64 + +# Initialize FastMCP server +mcp = FastMCP("OpenAI API") + +# OpenAI API configuration +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") +OPENAI_BASE_URL = "https://api.openai.com/v1" + +if not OPENAI_API_KEY: + raise ValueError("OPENAI_API_KEY environment variable is required") + + +async def make_openai_request( + method: str, + endpoint: str, + data: Optional[Dict[str, Any]] = None, + is_json: bool = True +) -> Any: + """Make a request to OpenAI API""" + headers = { + "Authorization": f"Bearer {OPENAI_API_KEY}", + } + if is_json: + headers["Content-Type"] = "application/json" + + url = f"{OPENAI_BASE_URL}/{endpoint}" + + async with httpx.AsyncClient(timeout=60.0) as client: + if method == "GET": + response = await client.get(url, headers=headers) + elif method == "POST": + if is_json: + response = await client.post(url, headers=headers, json=data) + else: + response = await client.post(url, headers=headers, data=data) + elif method == "DELETE": + response = await client.delete(url, headers=headers) + else: + raise ValueError(f"Unsupported HTTP method: {method}") + + response.raise_for_status() + return response.json() if is_json else response.content + + +@mcp.tool() +async def chat_completion( + messages: List[Dict[str, str]], + model: str = "gpt-4o-mini", + temperature: float = 1.0, + max_tokens: Optional[int] = None, + response_format: Optional[str] = None +) -> str: + """ + Generate a chat completion using OpenAI models. + + Args: + messages: List of message objects with 'role' and 'content' + Example: [{"role": "user", "content": "Hello!"}] + model: Model to use (gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-3.5-turbo, etc.) + temperature: Sampling temperature (0-2) + max_tokens: Maximum tokens in response + response_format: Optional response format ("json_object" for JSON mode) + + Returns: + The assistant's response message + """ + data = { + "model": model, + "messages": messages, + "temperature": temperature, + } + + if max_tokens: + data["max_tokens"] = max_tokens + + if response_format: + data["response_format"] = {"type": response_format} + + result = await make_openai_request("POST", "chat/completions", data) + return result["choices"][0]["message"]["content"] + + +@mcp.tool() +async def create_embedding( + text: str, + model: str = "text-embedding-3-small" +) -> Dict[str, Any]: + """ + Create an embedding vector for the given text. + + Args: + text: Text to embed + model: Embedding model (text-embedding-3-small, text-embedding-3-large, text-embedding-ada-002) + + Returns: + Dictionary with embedding vector and metadata + """ + data = { + "input": text, + "model": model + } + + result = await make_openai_request("POST", "embeddings", data) + return { + "embedding": result["data"][0]["embedding"], + "model": result["model"], + "dimensions": len(result["data"][0]["embedding"]), + "usage": result["usage"] + } + + +@mcp.tool() +async def generate_image( + prompt: str, + model: str = "dall-e-3", + size: str = "1024x1024", + quality: str = "standard", + n: int = 1 +) -> List[str]: + """ + Generate images using DALL-E. + + Args: + prompt: Text description of desired image + model: Model to use (dall-e-3, dall-e-2) + size: Image size (1024x1024, 1792x1024, 1024x1792 for dall-e-3) + quality: Image quality (standard or hd for dall-e-3) + n: Number of images to generate (1-10, only 1 for dall-e-3) + + Returns: + List of image URLs + """ + data = { + "prompt": prompt, + "model": model, + "size": size, + "n": n + } + + if model == "dall-e-3": + data["quality"] = quality + + result = await make_openai_request("POST", "images/generations", data) + return [img["url"] for img in result["data"]] + + +@mcp.tool() +async def text_to_speech( + text: str, + voice: str = "alloy", + model: str = "tts-1", + speed: float = 1.0 +) -> str: + """ + Convert text to speech audio. + + Args: + text: Text to convert to speech + voice: Voice to use (alloy, echo, fable, onyx, nova, shimmer) + model: TTS model (tts-1, tts-1-hd) + speed: Speech speed (0.25 to 4.0) + + Returns: + Base64 encoded audio data (MP3 format) + """ + data = { + "input": text, + "voice": voice, + "model": model, + "speed": speed + } + + audio_bytes = await make_openai_request("POST", "audio/speech", data, is_json=False) + return base64.b64encode(audio_bytes).decode('utf-8') + + +@mcp.tool() +async def transcribe_audio( + audio_file_base64: str, + model: str = "whisper-1", + language: Optional[str] = None, + response_format: str = "json" +) -> Dict[str, Any]: + """ + Transcribe audio to text using Whisper. + + Args: + audio_file_base64: Base64 encoded audio file + model: Whisper model to use + language: Language code (e.g., 'en', 'es') - auto-detected if not provided + response_format: Response format (json, text, srt, vtt, verbose_json) + + Returns: + Transcription result with text and metadata + """ + # Decode base64 audio + audio_bytes = base64.b64decode(audio_file_base64) + + # Prepare multipart form data + files = { + "file": ("audio.mp3", audio_bytes, "audio/mpeg"), + "model": (None, model), + "response_format": (None, response_format) + } + + if language: + files["language"] = (None, language) + + headers = {"Authorization": f"Bearer {OPENAI_API_KEY}"} + + async with httpx.AsyncClient(timeout=60.0) as client: + response = await client.post( + f"{OPENAI_BASE_URL}/audio/transcriptions", + headers=headers, + files=files + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def list_models() -> List[Dict[str, Any]]: + """ + List all available OpenAI models. + + Returns: + List of model objects with id, created date, and owned_by information + """ + result = await make_openai_request("GET", "models") + return result["data"] + + +@mcp.tool() +async def moderate_content( + text: str, + model: str = "text-moderation-latest" +) -> Dict[str, Any]: + """ + Check if content violates OpenAI's usage policies. + + Args: + text: Text to moderate + model: Moderation model (text-moderation-latest, text-moderation-stable) + + Returns: + Moderation results with category flags and scores + """ + data = { + "input": text, + "model": model + } + + result = await make_openai_request("POST", "moderations", data) + return result["results"][0] + + +@mcp.tool() +async def create_completion( + prompt: str, + model: str = "gpt-3.5-turbo-instruct", + max_tokens: int = 100, + temperature: float = 1.0, + stop: Optional[List[str]] = None +) -> str: + """ + Generate a completion (legacy endpoint, use chat_completion for newer models). + + Args: + prompt: The prompt to generate completion for + model: Model to use (gpt-3.5-turbo-instruct, davinci-002, babbage-002) + max_tokens: Maximum tokens to generate + temperature: Sampling temperature (0-2) + stop: Sequences where the API will stop generating + + Returns: + Generated completion text + """ + data = { + "model": model, + "prompt": prompt, + "max_tokens": max_tokens, + "temperature": temperature + } + + if stop: + data["stop"] = stop + + result = await make_openai_request("POST", "completions", data) + return result["choices"][0]["text"] + + +@mcp.tool() +async def analyze_image( + image_url: str, + prompt: str = "What's in this image?", + model: str = "gpt-4o-mini", + max_tokens: int = 300 +) -> str: + """ + Analyze an image using GPT-4 Vision. + + Args: + image_url: URL of the image to analyze + prompt: Question or instruction about the image + model: Vision-capable model (gpt-4o, gpt-4o-mini, gpt-4-turbo) + max_tokens: Maximum tokens in response + + Returns: + Analysis of the image + """ + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": prompt}, + {"type": "image_url", "image_url": {"url": image_url}} + ] + } + ] + + data = { + "model": model, + "messages": messages, + "max_tokens": max_tokens + } + + result = await make_openai_request("POST", "chat/completions", data) + return result["choices"][0]["message"]["content"] \ No newline at end of file diff --git a/servers/openai/test.json b/servers/openai/test.json new file mode 100644 index 0000000..609b617 --- /dev/null +++ b/servers/openai/test.json @@ -0,0 +1,43 @@ +{ + "environment": { + "OPENAI_API_KEY": "${OPENAI_API_KEY}" + }, + "tests": [ + { + "name": "Test chat completion", + "tool": "chat_completion", + "arguments": { + "messages": [ + {"role": "user", "content": "Say 'test successful' and nothing else"} + ], + "model": "gpt-4o-mini", + "max_tokens": 10 + }, + "expect": { + "type": "text", + "contains": "test successful" + } + }, + { + "name": "Test embedding creation", + "tool": "create_embedding", + "arguments": { + "text": "Hello, world!", + "model": "text-embedding-3-small" + }, + "expect": { + "type": "object", + "hasKeys": ["embedding", "model", "dimensions"] + } + }, + { + "name": "Test model listing", + "tool": "list_models", + "arguments": {}, + "expect": { + "type": "array", + "minLength": 1 + } + } + ] +} \ No newline at end of file