Skip to main content
Glama
twelvedata

Twelve Data MCP Server

Official
by twelvedata

GetCurrencyConversion

Convert currency amounts using real-time exchange rates for forex and cryptocurrency pairs. Specify a date for historical rates or get current conversions.

Instructions

This API call returns real-time exchange rate and converted amount for currency pair. Works with forex and cryptocurrency.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
paramsYes

Implementation Reference

  • Core handler logic executed by the generated GetCurrencyConversion tool function. Makes HTTP GET request to Twelve Data API endpoint '/currency_conversion' with resolved parameters and apikey, validates response with Pydantic model.
    async def _call_endpoint(
        endpoint: str,
        params: P,
        response_model: Type[R],
        ctx: Context
    ) -> R:
        params.apikey = extract_twelve_data_apikey(
            twelve_data_apikey=twelve_data_apikey,
            transport=transport,
            ctx=ctx
        )
    
        params_dict = params.model_dump(exclude_none=True)
        resolved_endpoint = resolve_path_params(endpoint, params_dict)
    
        async with httpx.AsyncClient(
            trust_env=False,
            headers={
                "accept": "application/json",
                "user-agent": "python-httpx/0.24.0"
            },
        ) as client:
            resp = await client.get(
                f"{api_base}/{resolved_endpoint}",
                params=params_dict
            )
            resp.raise_for_status()
            resp_json = resp.json()
    
            if isinstance(resp_json, dict):
                status = resp_json.get("status")
                if status == "error":
                    code = resp_json.get('code')
                    raise HTTPException(
                        status_code=code,
                        detail=f"Failed to perform request,"
                               f" code = {code}, message = {resp_json.get('message')}"
                    )
    
            return response_model.model_validate(resp_json)
  • Registers all generated MCP tools, including GetCurrencyConversion, passing the server and the generic _call_endpoint handler.
    register_all_tools(server=server, _call_endpoint=_call_endpoint)
  • Code generation script that creates the register_all_tools function and the specific tool registration for GetCurrencyConversion (name from operationId, endpoint '/currency_conversion', params/response models).
    lines.append('def register_all_tools(server: FastMCP, _call_endpoint):')
    for op, desc, key in ops:
        fixed_op = fix_case(op)
        lines += [
            f'    @server.tool(name="{op}",',
            f'                 description="{desc}")',
            f'    async def {op}(params: {fixed_op}Request, ctx: Context) -> {fixed_op}200Response:',
            f'        return await _call_endpoint("{key}", params, {fixed_op}200Response, ctx)',
            ''
        ]
    return '\n'.join(lines)
  • Generates input schema (Pydantic BaseModel) for tool parameters, including apikey, outputsize, etc., from OpenAPI parameters for operationId 'GetCurrencyConversion'.
    import json
    from pathlib import Path
    import keyword
    from typing import Any, List, Optional
    
    OPENAPI_PATH = "../extra/openapi_clean.json"
    REQUESTS_FILE = "../data/request_models.py"
    
    PRIMITIVES = {
        "string": "str",
        "integer": "int",
        "number": "float",
        "boolean": "bool",
        "object": "dict",
        "array": "list",
    }
    
    
    def canonical_class_name(opid: str, suffix: str) -> str:
        if not opid:
            return ""
        return opid[0].upper() + opid[1:] + suffix
    
    
    def safe_field_name(name: str) -> str:
        # Append underscore if name is a Python keyword
        if keyword.iskeyword(name):
            return name + "_"
        return name
    
    
    def python_type(schema: dict, components: dict) -> str:
        # Resolve $ref to the corresponding model class name
        if "$ref" in schema:
            ref_name = schema["$ref"].split("/")[-1]
            return canonical_class_name(ref_name, "")
        # Handle allOf by delegating to the first subschema
        if "allOf" in schema:
            for subschema in schema["allOf"]:
                return python_type(subschema, components)
        t = schema.get("type", "string")
        if t == "array":
            # Construct type for lists recursively
            return f"list[{python_type(schema.get('items', {}), components)}]"
        return PRIMITIVES.get(t, "Any")
    
    
    def resolve_schema(schema: dict, components: dict) -> dict:
        # Fully resolve $ref and allOf compositions into a merged schema
        if "$ref" in schema:
            ref = schema["$ref"].split("/")[-1]
            return resolve_schema(components.get(ref, {}), components)
        if "allOf" in schema:
            merged = {"properties": {}, "required": [], "description": ""}
            for subschema in schema["allOf"]:
                sub = resolve_schema(subschema, components)
                merged["properties"].update(sub.get("properties", {}))
                merged["required"].extend(sub.get("required", []))
                if sub.get("description"):
                    merged["description"] += sub["description"] + "\n"
            merged["required"] = list(set(merged["required"]))
            merged["description"] = merged["description"].strip() or None
            return merged
        return schema
    
    
    def collect_examples(param: dict, sch: dict) -> List[Any]:
        # Collect all examples from parameter, schema, and enums without deduplication
        examples: List[Any] = []
        if "example" in param:
            examples.append(param["example"])
        if "examples" in param:
            exs = param["examples"]
            if isinstance(exs, dict):
                for v in exs.values():
                    examples.append(v["value"] if isinstance(v, dict) and "value" in v else v)
            elif isinstance(exs, list):
                examples.extend(exs)
        if "example" in sch:
            examples.append(sch["example"])
        if "examples" in sch:
            exs = sch["examples"]
            if isinstance(exs, dict):
                for v in exs.values():
                    examples.append(v["value"] if isinstance(v, dict) and "value" in v else v)
            elif isinstance(exs, list):
                examples.extend(exs)
        # Include enum values as examples if present
        if "enum" in sch and isinstance(sch["enum"], list):
            examples.extend(sch["enum"])
        return [e for e in examples if e is not None]
    
    
    def gen_field(name: str, typ: str, required: bool, desc: Optional[str],
                  examples: List[Any], default: Any) -> str:
        name = safe_field_name(name)
        # Wrap in Optional[...] if default is None and field is not required
        if default is None and not required:
            typ = f"Optional[{typ}]"
        args: List[str] = []
        if required:
            args.append("...")
        else:
            args.append(f"default={repr(default)}")
        if desc:
            args.append(f"description={repr(desc)}")
        if examples:
            args.append(f"examples={repr(examples)}")
        return f"    {name}: {typ} = Field({', '.join(args)})"
    
    
    def gen_class(name: str, props: dict, desc: Optional[str]) -> str:
        lines = [f"class {name}(BaseModel):"]
        if desc:
            # Add class docstring if description is present
            lines.append(f'    """{desc.replace(chr(34)*3, "")}"""')
        if not props:
            lines.append("    pass")
        else:
            for pname, fdict in props.items():
                lines.append(gen_field(
                    pname,
                    fdict["type"],
                    fdict["required"],
                    fdict["description"],
                    fdict["examples"],
                    fdict["default"]
                ))
        return "\n".join(lines)
    
    
    def main():
        # Load the OpenAPI specification
        with open(OPENAPI_PATH, "r", encoding="utf-8") as f:
            spec = json.load(f)
    
        components = spec.get("components", {}).get("schemas", {})
        request_models: List[str] = []
        request_names: set = set()
    
        for path, methods in spec.get("paths", {}).items():
            for http_method, op in methods.items():
                opid = op.get("operationId")
                if not opid:
                    continue
                class_name = canonical_class_name(opid, "Request")
    
                # Collect parameters from path, query, header, etc.
                props: dict = {}
                for param in op.get("parameters", []):
                    name = param["name"]
                    sch = param.get("schema", {"type": "string"})
                    typ = python_type(sch, components)
                    required = param.get("required", False)
                    desc = param.get("description") or sch.get("description")
                    examples = collect_examples(param, sch)
                    default = sch.get("default", None)
                    props[name] = {
                        "type": typ,
                        "required": required,
                        "description": desc,
                        "examples": examples,
                        "default": default,
                    }
    
                # Collect JSON body properties
                body = op.get("requestBody", {}) \
                         .get("content", {}) \
                         .get("application/json", {}) \
                         .get("schema")
                if body:
                    body_sch = resolve_schema(body, components)
                    for name, sch in body_sch.get("properties", {}).items():
                        typ = python_type(sch, components)
                        required = name in body_sch.get("required", [])
                        desc = sch.get("description")
                        examples = collect_examples({}, sch)
                        default = sch.get("default", None)
                        props[name] = {
                            "type": typ,
                            "required": required,
                            "description": desc,
                            "examples": examples,
                            "default": default,
                        }
    
                if "outputsize" not in props:
                    props["outputsize"] = {
                        "type": "int",
                        "required": False,
                        "description": (
                            "Number of data points to retrieve. Supports values in the range from `1` to `5000`. "
                            "Default `10` when no date parameters are set, otherwise set to maximum"
                        ),
                        "examples": [10],
                        "default": 10,
                    }
                else:
                    props["outputsize"]["default"] = 10
                    props["outputsize"]["description"] = props["outputsize"]["description"].replace(
                        'Default `30`', 'Default `10`'
                    )
                    props["outputsize"]["examples"] = [10]
    
                # Add apikey with default="demo"
                props["apikey"] = {
                    "type": "str",
                    "required": False,
                    "description": "API key",
                    "examples": ["demo"],
                    "default": "demo",
                }
    
                if "interval" in props:
                    props["interval"]["required"] = False
                    props["interval"]["default"] = "1day"
    
                # Append plan availability to the description if x-starting-plan is present
                starting_plan = op.get("x-starting-plan")
                description = op.get("description", "")
                if starting_plan:
                    addon = f" Available starting from the `{starting_plan}` plan."
                    description = (description or "") + addon
    
                code = gen_class(class_name, props, description)
    
                if class_name not in request_names:
                    request_models.append(code)
                    request_names.add(class_name)
    
        # Write all generated models to the target file
        header = (
            "from pydantic import BaseModel, Field\n"
            "from typing import Any, List, Optional\n\n"
        )
        Path(REQUESTS_FILE).write_text(header + "\n\n".join(request_models), encoding="utf-8")
        print(f"Generated request models: {REQUESTS_FILE}")
  • Generates output schema (Pydantic BaseModel) for tool responses from OpenAPI using datamodel-codegen.
    import subprocess
    from pathlib import Path
    
    openapi_path = '../extra/openapi_clean.json'
    output_path = '../data/response_models.py'
    
    cmd = [
        'datamodel-codegen',
        '--input', str(openapi_path),
        '--input-file-type', 'openapi',
        '--output', str(output_path),
        '--output-model-type', 'pydantic_v2.BaseModel',
        '--reuse-model',
        '--use-title-as-name',
        '--disable-timestamp',
        '--field-constraints',
        '--use-double-quotes',
    ]
    
    subprocess.run(cmd, check=True)
    
    # Append aliases
    alias_lines = [
        '',
        '# Aliases for response models',
        'GetMarketMovers200Response = MarketMoversResponseBody',
        'GetTimeSeriesPercent_B200Response = GetTimeSeriesPercentB200Response',
        ''
    ]
    
    with open(output_path, 'a', encoding='utf-8') as f:
        f.write('\n'.join(alias_lines))
    
    print(f"[SUCCESS] Models generated using CLI and aliases added: {output_path}")
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. While it mentions 'real-time exchange rate' and works with 'forex and cryptocurrency,' it doesn't disclose important behavioral traits like rate limits, authentication requirements (though apikey parameter exists), data freshness guarantees, error conditions, or response format details. The description is too sparse for a tool with 9 parameters and no annotation coverage.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately concise with just two sentences that directly state the tool's core functionality. There's no wasted verbiage or unnecessary elaboration. However, it could be better structured by front-loading the most critical information more explicitly, but overall it's efficient and to the point.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (9 parameters, no output schema, no annotations), the description is incomplete. It doesn't explain what the tool returns beyond 'exchange rate and converted amount,' doesn't clarify the relationship between parameters, and doesn't address important contextual factors like data sources, accuracy, or limitations. For a financial data tool with multiple parameters affecting output format and historical data retrieval, this description leaves too many questions unanswered.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters1/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description provides zero information about parameters beyond what's already in the schema. With 0% schema description coverage (meaning schema properties lack descriptions), the description should compensate by explaining key parameters, but it doesn't mention any parameters at all. For a tool with 9 parameters including complex ones like date, timezone, and outputsize, this is a significant gap that leaves the agent guessing about parameter meanings and usage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'returns real-time exchange rate and converted amount for currency pair' with the specific scope of 'Works with forex and cryptocurrency.' This provides a specific verb (returns), resource (exchange rate and converted amount), and scope. However, it doesn't explicitly distinguish this tool from sibling tools like GetExchangeRate or GetForexPairs, which appear to offer similar functionality.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. With multiple sibling tools that appear related to currency/exchange data (GetExchangeRate, GetForexPairs, GetCryptocurrencies), the description fails to explain what makes this tool unique or when it should be preferred over those alternatives. There's no mention of prerequisites, limitations, or comparative use cases.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/twelvedata/mcp'

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