Skip to main content
Glama
server.py31.1 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.detail: Get document details - module.get: Get application module information (version 1.0) - doc.list: List all documents in application (version 1.0) - doc.details: Get multiple document details (version 1.0) Environment Variables Required: - TORNA_URL: Torna private deployment URL (default: "http://localhost:7700/api") - TORNA_TOKEN: Single module token for authentication """ import asyncio import json import os import urllib.parse from enum import Enum from typing import Any, Dict, List, Optional import httpx from mcp.server.fastmcp import FastMCP from pydantic import BaseModel, ConfigDict, Field # Initialize the MCP server torna_mcp_server = FastMCP("torna_mcp") # Constants CHARACTER_LIMIT = 25000 DEFAULT_API_URL = "http://localhost:7700/api" # Environment variables API_BASE_URL: Optional[str] = None TORNA_TOKEN: str = "" def _validate_environment() -> tuple[str, str]: """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" ) # Additional input models for new API interfaces class ModuleGetInput(BaseModel): """Input model for module.get operation.""" model_config = ConfigDict(str_strip_whitespace=True) response_format: ResponseFormat = Field(default=ResponseFormat.MARKDOWN, description="Output format") class DocListInput(BaseModel): """Input model for doc.list operation.""" model_config = ConfigDict(str_strip_whitespace=True) doc_ids: Optional[List[str]] = Field(default_factory=list, description="Document IDs to list (optional)") response_format: ResponseFormat = Field(default=ResponseFormat.MARKDOWN, description="Output format") class DocDetailsInput(BaseModel): """Input model for doc.details operation.""" model_config = ConfigDict(str_strip_whitespace=True) doc_ids: List[str] = Field(..., description="Document IDs to retrieve details for") 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.detail": 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')}") # Handle parameters based on TornaDocParamDTO structure params_by_style = { 0: ("Header Parameters", []), 1: ("Request Parameters", []), 2: ("Response Parameters", []), 3: ("Error Code Parameters", []) } for param in doc.get("requestParams", []): style = param.get("style", 1) # Default to request params params_by_style[style][1].append(param) for style_name, params in params_by_style.items(): if params[1]: # If there are parameters of this type lines.append(f"\n### {params[0]}") for param in params[1]: required_mark = "⚠️ " if param.get("required") == 1 else "✅ " lines.append( f"- **{param.get('name', 'N/A')}** ({param.get('type', 'N/A')}) {required_mark}" ) if param.get("description"): lines.append(f" - {param.get('description')}") if param.get("example"): lines.append(f" - Example: {param.get('example')}") if param.get("maxLength"): lines.append(f" - Max Length: {param.get('maxLength')}") else: lines.append("Document not found.") elif interface_name == "module.get": module_info = result.get("data", {}) if module_info: lines.append(f"## {module_info.get('name', 'Module Information')}") lines.append(f"- **Module ID**: {module_info.get('id', 'N/A')}") lines.append(f"- **Description**: {module_info.get('description', 'N/A')}") lines.append(f"- **Status**: {module_info.get('status', 'N/A')}") else: lines.append("Module information not found.") elif interface_name == "doc.list": docs = result.get("data", []) if docs: lines.append(f"## Document List ({len(docs)} documents)") lines.append("") for doc in docs: folder_status = "📁" if doc.get("isFolder") == 1 else "📄" lines.append(f"{folder_status} **{doc.get('name', 'N/A')}**") lines.append(f" - **ID**: {doc.get('id', 'N/A')}") if doc.get("url"): lines.append(f" - **URL**: {doc.get('url', 'N/A')}") lines.append(f" - **Method**: {doc.get('httpMethod', 'N/A')}") lines.append(f" - **Version**: {doc.get('version', 'N/A')}") if doc.get("description"): lines.append(f" - **Description**: {doc.get('description')}") lines.append("") else: lines.append("No documents found.") elif interface_name == "doc.details": docs = result.get("data", []) if docs: lines.append(f"## Document Details ({len(docs)} documents)") lines.append("") for doc in docs: 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"- **Version**: {doc.get('version', 'N/A')}") if doc.get("description"): lines.append(f"- **Description**: {doc.get('description')}") # Group parameters by style params_by_style = { 0: ("Header Parameters", []), 1: ("Request Parameters", []), 2: ("Response Parameters", []), 3: ("Error Code Parameters", []) } for param in doc.get("requestParams", []): style = param.get("style", 1) if style in params_by_style: params_by_style[style][1].append(param) for style_name, params in params_by_style.items(): if params[1]: lines.append(f"\n#### {params[0]}") for param in params[1]: required_mark = "⚠️ " if param.get("required") == 1 else "✅ " lines.append( f"- **{param.get('name', 'N/A')}** ({param.get('type', 'N/A')}) {required_mark}" ) if param.get("description"): lines.append(f" - {param.get('description')}") if param.get("example"): lines.append(f" - Example: {param.get('example')}") if param.get("maxLength"): lines.append(f" - Max Length: {param.get('maxLength')}") lines.append("") else: lines.append("No document details 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 @torna_mcp_server.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_detail 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) @torna_mcp_server.tool( name="torna_get_document_detail", annotations={ "title": "Get Document from Torna", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True, }, ) async def torna_get_document_detail(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.detail 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.detail", version="1.0", data=data) return _format_response(result, params.response_format, "doc.detail") except Exception as e: return _handle_api_error(e) @torna_mcp_server.tool( name="torna_get_module", annotations={ "title": "Get Application Module Information", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True, }, ) async def torna_get_module(params: ModuleGetInput) -> str: """Get application module information from Torna. This tool retrieves basic information about the current module/application based on the real Torna API module.get interface (version 1.0). Args: params: Validated input parameters (currently no required parameters) Returns: str: JSON-formatted or markdown-formatted response containing module information Success response: { "code": 0, "msg": "success", "data": { "name": "Module Name", "description": "Module Description", "id": "module_id", "status": "active" } } Error response: "Error: <error message>" Examples: - Use when: Getting basic information about the current module - Use when: Verifying module name and details - Don't use when: You need document details (use torna_get_document_detail instead) Error Handling: - Input validation errors are handled by Pydantic model - Returns "Error: Permission denied" if access token is invalid - Returns formatted success or error message """ try: # Module.get doesn't require specific parameters result = _make_api_request( interface_name="module.get", version="1.0", data={} ) return _format_response(result, params.response_format, "module.get") except Exception as e: return _handle_api_error(e) @torna_mcp_server.tool( name="torna_list_documents", annotations={ "title": "List All Documents in Application", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True, }, ) async def torna_list_documents(params: DocListInput) -> str: """List all documents in the application from Torna. This tool retrieves a comprehensive list of all documents available in the current module based on the real Torna API doc.list interface (version 1.0). This is the key tool that solves the "获取所有文档详情" requirement! Args: params: Validated input parameters (currently no required parameters) Returns: str: JSON-formatted or markdown-formatted response containing document list Success response: { "code": 0, "msg": "success", "data": [ { "id": "doc_123", "name": "Document Name", "url": "/api/endpoint", "httpMethod": "GET", "description": "Document description" } ] } Error response: "Error: <error message>" Examples: - Use when: Getting a complete list of all documents in the module - Use when: Discovering document IDs for subsequent detail retrieval - Use when: Managing document inventory - Don't use when: You need detailed parameters (use torna_get_document_detail_batch instead) Error Handling: - Input validation errors are handled by Pydantic model - Returns "Error: Permission denied" if access token is invalid - Returns formatted document list or error message """ try: # Doc.list requires docIds parameter (even if empty) data = {"docIds": params.doc_ids or []} result = _make_api_request( interface_name="doc.list", version="1.0", data=data ) return _format_response(result, params.response_format, "doc.list") except Exception as e: return _handle_api_error(e) @torna_mcp_server.tool( name="torna_get_document_detail_batch", annotations={ "title": "Get Multiple Document Details", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True, }, ) async def torna_get_document_detail_batch(params: DocDetailsInput) -> str: """Get detailed information for multiple documents at once. This tool retrieves comprehensive details for multiple documents simultaneously based on the real Torna API doc.details interface (version 1.0). This is perfect for getting all document details after listing them! Args: params (DocDetailsInput): Validated input parameters containing: - doc_ids (List[str]): Array of document IDs to retrieve details for - response_format (str): Output format (markdown or json) Returns: str: JSON-formatted or markdown-formatted response containing detailed document information Success response: { "code": 0, "msg": "success", "data": [ { "id": "doc_123", "name": "Document Name", "url": "/api/endpoint", "httpMethod": "GET", "requestParams": [...], "responseParams": [...], "errorCodeParams": [...] } ] } Error response: "Error: <error message>" Examples: - Use when: Getting detailed parameters for multiple documents - Use when: Bulk processing document information - Use when: After using torna_list_documents to get all document IDs - Don't use when: You only need basic information (use torna_list_documents instead) Error Handling: - Input validation errors are handled by Pydantic model - Returns "Error: Permission denied" if access token is invalid - Returns "Error: Resource not found" if any document doesn't exist - Returns formatted detailed document information """ try: # Format data for doc.details API data = { "ids": params.doc_ids } result = _make_api_request( interface_name="doc.details", version="1.0", data=data ) return _format_response(result, params.response_format, "doc.details") 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: toma-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_detail: Get single document details") print(" - torna_get_module: Get application module information") print(" - torna_list_documents: List all documents in application") print(" - torna_get_document_detail_batch: Get multiple document details") return elif sys.argv[1] in ["--version", "-v"]: print("toma-mcp version 0.1.0") 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 asyncio.run(torna_mcp_server.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)

Latest Blog Posts

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