Skip to main content
Glama

Torna MCP Server

by li7hai26
main.py18.6 kB
#!/usr/bin/env python3 """ Torna MCP Server This MCP server provides tools to interact with Torna OpenAPI for managing API documentation. Based on the real Torna API at http://localhost:7700/api with correct interface specifications. Real Torna API interfaces: - doc.push: Push documents to Torna - doc.get: Get document details Environment Variables Required: - TORNA_URL: Torna private deployment URL (default: "http://localhost:7700/api") - TORNA_TOKEN: Single module token for authentication """ import os import json import urllib.parse from typing import List, Dict, Any, Optional from enum import Enum import httpx from pydantic import BaseModel, Field, ConfigDict from mcp.server.fastmcp import FastMCP # Initialize the MCP server mcp = FastMCP("torna_mcp") # Constants CHARACTER_LIMIT = 25000 DEFAULT_API_URL = "http://localhost:7700/api" # Environment variables (will be set in main function) API_BASE_URL: Optional[str] = None TORNA_TOKEN: str = "" def _validate_environment(): """Validate required environment variables.""" global API_BASE_URL, TORNA_TOKEN API_BASE_URL = os.getenv("TORNA_URL", DEFAULT_API_URL) TORNA_TOKEN = os.getenv("TORNA_TOKEN", "") if not TORNA_TOKEN: raise ValueError("TORNA_TOKEN environment variable is required") return API_BASE_URL, TORNA_TOKEN # Enums class ResponseFormat(str, Enum): """Output format for tool responses.""" MARKDOWN = "markdown" JSON = "json" class HttpMethod(str, Enum): """HTTP methods supported by Torna.""" GET = "GET" POST = "POST" PUT = "PUT" DELETE = "DELETE" PATCH = "PATCH" # Pydantic Models for Input Validation class DocPushInput(BaseModel): """Input model for document push operation.""" model_config = ConfigDict(str_strip_whitespace=True) # Document basic info name: str = Field(..., description="Document name", min_length=1, max_length=100) description: Optional[str] = Field(default=None, description="Document description") url: str = Field(..., description="API endpoint URL (e.g., '/api/users')") http_method: HttpMethod = Field(default=HttpMethod.GET, description="HTTP method") content_type: str = Field(default="application/json", description="Content type") is_folder: bool = Field(default=False, description="Whether this is a folder/category") parent_id: Optional[str] = Field(default=None, description="Parent category ID") is_show: bool = Field(default=True, description="Whether to show this document") # Request parameters request_params: Optional[List[Dict[str, Any]]] = Field(default_factory=list, description="Request parameters") header_params: Optional[List[Dict[str, Any]]] = Field(default_factory=list, description="Header parameters") path_params: Optional[List[Dict[str, Any]]] = Field(default_factory=list, description="Path parameters") query_params: Optional[List[Dict[str, Any]]] = Field(default_factory=list, description="Query parameters") # Response parameters response_params: Optional[List[Dict[str, Any]]] = Field(default_factory=list, description="Response parameters") # Error codes error_codes: Optional[List[Dict[str, str]]] = Field(default_factory=list, description="Error codes") # Debug environment debug_env_name: Optional[str] = Field(default=None, description="Debug environment name") debug_env_url: Optional[str] = Field(default=None, description="Debug environment URL") # Common error codes (applies to all documents in this push) common_error_codes: Optional[List[Dict[str, str]]] = Field(default_factory=list, description="Common error codes") # Author author: Optional[str] = Field(default=None, description="Document author") # Response format response_format: ResponseFormat = Field(default=ResponseFormat.MARKDOWN, description="Output format") class DocGetInput(BaseModel): """Input model for document get operation.""" model_config = ConfigDict(str_strip_whitespace=True) doc_id: str = Field(..., description="Document ID to retrieve") response_format: ResponseFormat = Field(default=ResponseFormat.MARKDOWN, description="Output format") # Shared utility functions def _make_api_request(interface_name: str, version: str, data: Dict[str, Any]) -> Dict[str, Any]: """Make request to Torna API with correct format.""" # Torna API expects data to be URL-encoded JSON string json_data = json.dumps(data, ensure_ascii=False) encoded_data = urllib.parse.quote(json_data) request_data = { "name": interface_name, "version": version, "data": encoded_data, "access_token": TORNA_TOKEN } with httpx.Client() as client: response = client.post( API_BASE_URL, json=request_data, timeout=30.0, headers={"Content-Type": "application/json"} ) response.raise_for_status() return response.json() def _handle_api_error(e: Exception) -> str: """Consistent error formatting across all tools.""" if isinstance(e, httpx.HTTPStatusError): if e.response.status_code == 404: return "Error: Resource not found. Please check the ID is correct." elif e.response.status_code == 403: return "Error: Permission denied. You don't have access to this resource." elif e.response.status_code == 429: return "Error: Rate limit exceeded. Please wait before making more requests." return f"Error: API request failed with status {e.response.status_code}" elif isinstance(e, httpx.TimeoutException): return "Error: Request timed out. Please try again." elif isinstance(e, ValueError) and "TORNA" in str(e): return f"Configuration error: {str(e)}" return f"Error: Unexpected error occurred: {type(e).__name__}" def _format_doc_push_data(input_data: DocPushInput) -> Dict[str, Any]: """Format input data for doc.push API according to Torna specification.""" doc_data = { "name": input_data.name, "description": input_data.description or "", "url": input_data.url, "httpMethod": input_data.http_method.value, "contentType": input_data.content_type, "isFolder": input_data.is_folder, "isShow": input_data.is_show } if input_data.parent_id: doc_data["parentId"] = input_data.parent_id if input_data.author: doc_data["author"] = input_data.author # Set parameters if provided if input_data.request_params: doc_data["requestParams"] = input_data.request_params if input_data.header_params: doc_data["headerParams"] = input_data.header_params if input_data.path_params: doc_data["pathParams"] = input_data.path_params if input_data.query_params: doc_data["queryParams"] = input_data.query_params if input_data.response_params: doc_data["responseParams"] = input_data.response_params if input_data.error_codes: doc_data["errorCodeParams"] = input_data.error_codes # Handle debug environment if input_data.debug_env_name and input_data.debug_env_url: doc_data["debugEnv"] = { "name": input_data.debug_env_name, "url": input_data.debug_env_url } return {"apis": [doc_data]} def _format_response(result: Dict[str, Any], response_format: ResponseFormat, interface_name: str) -> str: """Format API response based on requested format.""" if response_format == ResponseFormat.JSON: return json.dumps(result, indent=2, ensure_ascii=False) # Markdown format lines = [f"# {interface_name} Result", ""] # Check if this is a successful response if result.get("code") == 0 or result.get("code") == "0": lines.append("✅ **Operation completed successfully**") lines.append("") # Handle different response types if interface_name == "doc.push": if result.get("data"): lines.append("## Push Result") lines.append(f"- **Document Name**: {result['data'].get('name', 'N/A')}") lines.append(f"- **Document ID**: {result['data'].get('id', 'N/A')}") lines.append(f"- **Status**: {result['data'].get('status', 'N/A')}") else: lines.append("Documents have been pushed successfully.") elif interface_name == "doc.get": doc = result.get("data", {}) if doc: lines.append(f"## {doc.get('name', 'Document Detail')}") lines.append(f"- **ID**: {doc.get('id', 'N/A')}") lines.append(f"- **URL**: {doc.get('url', 'N/A')}") lines.append(f"- **Method**: {doc.get('httpMethod', 'N/A')}") lines.append(f"- **Content Type**: {doc.get('contentType', 'N/A')}") if doc.get('description'): lines.append(f"- **Description**: {doc.get('description')}") request_params = doc.get('requestParams', []) if request_params: lines.append("") lines.append("### Request Parameters") for param in request_params: lines.append(f"- **{param.get('name', 'N/A')}** ({param.get('type', 'N/A')})") if param.get('description'): lines.append(f" - {param.get('description')}") if param.get('example'): lines.append(f" - Example: {param.get('example')}") response_params = doc.get('responseParams', []) if response_params: lines.append("") lines.append("### Response Parameters") for param in response_params: lines.append(f"- **{param.get('name', 'N/A')}** ({param.get('type', 'N/A')})") if param.get('description'): lines.append(f" - {param.get('description')}") if param.get('example'): lines.append(f" - Example: {param.get('example')}") else: lines.append("Document not found.") else: lines.append("❌ **Operation failed**") lines.append("") lines.append(f"- **Error Code**: {result.get('code', 'Unknown')}") lines.append(f"- **Error Message**: {result.get('msg', 'Unknown error')}") response_text = "\n".join(lines) # Check character limit if len(response_text) > CHARACTER_LIMIT: truncated_text = response_text[:CHARACTER_LIMIT - 100] truncated_text += "\n\n... (response truncated due to length limit)" return truncated_text return response_text # Tool implementations @mcp.tool( name="torna_push_document", annotations={ "title": "Push Document to Torna", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": True } ) async def torna_push_document(params: DocPushInput) -> str: """Push a document to Torna platform. This tool creates or updates API documentation in Torna. Based on the real Torna API doc.push interface at http://localhost:7700/api. Args: params (DocPushInput): Validated input parameters containing: - name (str): Document name (required) - description (str, optional): Document description - url (str): API endpoint URL (e.g., '/api/users') - http_method (str): HTTP method (GET, POST, PUT, DELETE, PATCH) - content_type (str): Content type (default: 'application/json') - is_folder (bool): Whether this is a folder/category (default: False) - parent_id (str, optional): Parent category ID - is_show (bool): Whether to show this document (default: True) - request_params (list, optional): Request parameters with structure: [{"name": "param1", "type": "string", "description": "param desc", "required": true, "example": "value"}] - header_params (list, optional): Header parameters - path_params (list, optional): Path parameters - query_params (list, optional): Query parameters - response_params (list, optional): Response parameters - error_codes (list, optional): Error codes with structure: [{"code": "1001", "msg": "error message", "solution": "solution"}] - debug_env_name (str, optional): Debug environment name - debug_env_url (str, optional): Debug environment URL - common_error_codes (list, optional): Common error codes for all documents - author (str, optional): Document author Returns: str: JSON-formatted or markdown-formatted response containing operation results Success response: { "code": 0, "msg": "success", "data": { "id": "doc_id", "name": "document_name", "status": "created/updated" } } Error response: "Error: <error message>" Examples: - Use when: Creating new API documentation - Use when: Organizing documents into categories - Use when: Adding request/response parameter documentation - Don't use when: You only want to get existing documents (use torna_get_document instead) Error Handling: - Input validation errors are handled by Pydantic model - Returns "Error: Permission denied" if access token is invalid (403 status) - Returns "Error: Resource not found" if parent category doesn't exist (404 status) - Returns formatted success or error message """ try: # Format data for Torna API data = _format_doc_push_data(params) # Add common error codes if provided if params.common_error_codes: data["commonErrorCodes"] = params.common_error_codes # Make API request result = _make_api_request( interface_name="doc.push", version="1.0", data=data ) return _format_response(result, params.response_format, "doc.push") except Exception as e: return _handle_api_error(e) @mcp.tool( name="torna_get_document", annotations={ "title": "Get Document from Torna", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True } ) async def toma_get_document(params: DocGetInput) -> str: """Get detailed information about a specific document in Torna. This tool retrieves comprehensive details about a single document including request parameters, response parameters, headers, and error codes. Based on the real Torna API doc.get interface. Args: params (DocGetInput): Validated input parameters containing: - doc_id (str): Document ID to retrieve (required) Returns: str: JSON-formatted or markdown-formatted response containing detailed document information Success response: { "code": 0, "msg": "success", "data": { "id": "doc_id", "name": "document_name", "url": "/api/endpoint", "httpMethod": "GET", "description": "document description", "contentType": "application/json", "requestParams": [...], "responseParams": [...], "headerParams": [...], "errorCodeParams": [...] } } Error response: "Error: <error message>" Examples: - Use when: Getting full documentation for a specific API endpoint - Use when: Reviewing request/response parameters for an API - Use when: Checking error codes and examples - Don't use when: You need to create new documents (use torna_push_document instead) Error Handling: - Input validation errors are handled by Pydantic model - Returns "Error: Permission denied" if access token is invalid (403 status) - Returns "Error: Resource not found" if document doesn't exist (404 status) - Returns formatted detailed document information """ try: # Format data for Torna API data = { "id": params.doc_id } # Make API request result = _make_api_request( interface_name="doc.get", version="1.0", data=data ) return _format_response(result, params.response_format, "doc.get") except Exception as e: return _handle_api_error(e) # Main function def main(): """Main function to start the MCP server.""" # Check for help or version flags first import sys if len(sys.argv) > 1 and (sys.argv[1] in ['--help', '-h', '--version', '-v']): if sys.argv[1] in ['--help', '-h']: print("Torna MCP Server - Help") print("Usage: torna-mcp") print("") print("Environment Variables:") print(" TORNA_URL: Torna API base URL (default: http://localhost:7700/api)") print(" TORNA_TOKEN: Torna module token (required)") print("") print("Available tools:") print(" - torna_push_document: Push documents to Torna") print(" - torna_get_document: Get document details") return try: # Validate environment _validate_environment() print(f"Starting Torna MCP Server...") print(f"API Base URL: {API_BASE_URL}") print(f"Token configured: {'*' * 8}{TORNA_TOKEN[-4:] if TORNA_TOKEN else 'None'}") # Run the server mcp.run() except ValueError as e: print(f"Configuration error: {e}") print("Please set TORNA_TOKEN environment variable") print("Usage: export TORNA_TOKEN='your-token-here'") sys.exit(1) except Exception as e: print(f"Failed to start server: {e}") sys.exit(1) if __name__ == "__main__": main()

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/li7hai26/torna-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server