From 2245a8e04500f09440ff21cfef6f4913e8b9073a Mon Sep 17 00:00:00 2001 From: ckoegel Date: Tue, 15 Jul 2025 10:33:07 -0400 Subject: [PATCH 01/33] SWI-8019 First Pass at MCP --- .gitignore | 90 ++++++++++++++++++++++++++++++++ README.md | 106 ++++++++++++++++++++++++++++++++++++++ app.py | 26 ++++++++++ inspect.sh | 4 ++ requirements.txt | 42 +++++++++++++++ resources.py | 21 ++++++++ servers.py | 73 ++++++++++++++++++++++++++ utils.py | 130 +++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 492 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.py create mode 100644 inspect.sh create mode 100644 requirements.txt create mode 100644 resources.py create mode 100644 servers.py create mode 100644 utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34d1312 --- /dev/null +++ b/.gitignore @@ -0,0 +1,90 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..11f13b4 --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +# Bandwidth Official MCP Server +Source code for the official Bandwidth Model Context Protocol (MCP) Server. This server can be used to interact with different Bandwidth APIs via an AI agent. The server is provided as a PyPi package but may also be cloned directly from this repo. + +## Installation + +### Install from PyPi + + +### Install Locally + +Clone directly from this git repository using: + +```shell +git clone https://github.com/Bandwidth/bandwidth-mcp-server.git +cd bandwidth-mcp-server +``` + +## Prerequisites + +In order to use the Bandwidth MCP Server, you'll need the following things, set as environment variables. +- Valid Bandwidth API Credentials + - For more info on creating API credentials, see our [Credentials](https://dev.bandwidth.com/docs/credentials) page +- Bandwidth Account ID + +Conditional Variables: +- Messaging Application ID + - Necessary when using our Messaging API +- Bandwidth Telephone Number + - + +## Getting Started + +### Running the Server Standalone + +The MCP server can be run locally using either native python or uv. + +#### Run Using Native Python + +Running the server locally with a python [virtual environment](https://docs.python.org/3/library/venv.html) requires both [python](https://www.python.org/downloads/) and [pip](https://pip.pypa.io/en/stable/getting-started/). +Once these are installed, create a virtual environment using: + +```sh +python -m venv .venv +``` + +Then activate and install the required packaged from the `requirements.txt` file. + +```sh +source .venv/bin/activate +pip install -r requirements.txt +``` + +After all packages are installed in the virtual environment, you can run the server locally using: + +```sh +python app.py +``` + +#### Run Using uv + +### Usage with Claude + + +### Usage with goose + + +### Configuration + +### Environment Variables + +#### Including or Excluding APIs + +#### Including or Excluding Workflows + +## List of all Tools Supplied by the Server + +## **Multi-Factor Authentication (MFA)** +- `bw-mcp-server__generateMessagingCode` - Send MFA code via SMS +- `bw-mcp-server__generateVoiceCode` - Send MFA code via voice call +- `bw-mcp-server__verifyCode` - Verify a previously sent MFA code + +## **Phone Number Lookup** +- `bw-mcp-server__createLookup` - Create a phone number lookup request +- `bw-mcp-server__getLookupStatus` - Get status of an existing lookup request + +## **Voice & Call Management** +- `bw-mcp-server__listCalls` - Returns a list of call events with filtering options +- `bw-mcp-server__listCall` - Returns details for a single call event + +## **Reporting & Analytics** +- `bw-mcp-server__getReports` - Get history of created reports +- `bw-mcp-server__createReport` - Create a new report instance +- `bw-mcp-server__getReportStatus` - Get status of a report +- `bw-mcp-server__getReportFile` - Download report file +- `bw-mcp-server__getReportDefinitions` - Get available report definitions + +## **Media Management** +- `bw-mcp-server__listMedia` - List your media files +- `bw-mcp-server__getMedia` - Download a specific media file +- `bw-mcp-server__uploadMedia` - Upload a media file +- `bw-mcp-server__deleteMedia` - Delete a media file + +## **Messaging** +- `bw-mcp-server__listMessages` - List messages with filtering options +- `bw-mcp-server__createMessage` - Send SMS/MMS messages +- `bw-mcp-server__createMultiChannelMessage` - Send multi-channel messages (RBM, SMS, MMS) diff --git a/app.py b/app.py new file mode 100644 index 0000000..b8bfcd9 --- /dev/null +++ b/app.py @@ -0,0 +1,26 @@ +import asyncio + +from fastmcp import FastMCP + +from utils import get_enabled_apis, get_excluded_apis +from resources import config_resource +from servers import create_bandwidth_mcp + +mcp = FastMCP(name="Bandwidth MCP") + +# Initialize the Bandwidth API client +async def setup(mcp: FastMCP = mcp): + enabled_apis = get_enabled_apis() + excluded_apis = get_excluded_apis() + + await create_bandwidth_mcp( + mcp, + enabled_apis, + excluded_apis + ) + +mcp.add_resource(config_resource) + +if __name__ == "__main__": + asyncio.run(setup()) + mcp.run() diff --git a/inspect.sh b/inspect.sh new file mode 100644 index 0000000..72b284e --- /dev/null +++ b/inspect.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +npx @modelcontextprotocol/inspector \ + /Users/ckoegel/Documents/repositories/misc/mcp-poc/.venv/bin/python /Users/ckoegel/Documents/repositories/misc/mcp-poc/app.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..14a2dc8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,42 @@ +annotated-types==0.7.0 +anyio==4.9.0 +Authlib==1.6.0 +bandwidth-sdk==19.1.0 +certifi==2025.6.15 +cffi==1.17.1 +charset-normalizer==3.4.2 +click==8.2.1 +cryptography==45.0.4 +exceptiongroup==1.3.0 +fastmcp==2.9.0 +h11==0.16.0 +httpcore==1.0.9 +httpx==0.28.1 +httpx-sse==0.4.1 +idna==3.10 +load-dotenv==0.1.0 +markdown-it-py==3.0.0 +mcp==1.9.4 +mdurl==0.1.2 +openapi-pydantic==0.5.1 +pycparser==2.22 +pydantic==2.11.7 +pydantic-settings==2.10.1 +pydantic_core==2.33.2 +Pygments==2.19.2 +python-dateutil==2.9.0.post0 +python-dotenv==1.1.1 +python-multipart==0.0.20 +PyYAML==6.0.2 +requests==2.32.4 +rich==14.0.0 +shellingham==1.5.4 +six==1.17.0 +sniffio==1.3.1 +sse-starlette==2.3.6 +starlette==0.47.1 +typer==0.16.0 +typing-inspection==0.4.1 +typing_extensions==4.14.0 +urllib3==2.5.0 +uvicorn==0.34.3 diff --git a/resources.py b/resources.py new file mode 100644 index 0000000..4a1fe82 --- /dev/null +++ b/resources.py @@ -0,0 +1,21 @@ +from fastmcp.resources import FunctionResource, HttpResource + +from utils import get_config + +config_resource = FunctionResource( + name="Bandwidth API Configuration", + description="Configuration Object for Bandwidth API", + tags={"bandwidth", "config"}, + uri="data://config", + mime_type="application/json", + fn=get_config, +) + +number_order_guide_resource = HttpResource( + name="Bandwidth Number Order Guide", + description="Bandwidth Number Order Guide", + tags={"bandwidth", "number", "order", "guide"}, + uri="data://number_order_guide", + mime_type="text/markdown", + url="https://dev.bandwidth.com/docs/numbers/guides/searchingForNumbers.md", +) diff --git a/servers.py b/servers.py new file mode 100644 index 0000000..f8f8a08 --- /dev/null +++ b/servers.py @@ -0,0 +1,73 @@ +import yaml +import httpx +import base64 +import requests +from fastmcp import FastMCP +from utils import username, password, clean_openapi_spec, filter_apis +from resources import number_order_guide_resource + +api_server_info = { + "messaging": { + "url": "https://dev.bandwidth.com/spec/messaging.yml", + "resources": [number_order_guide_resource] + }, + "multi-factor-auth": { + "url": "https://dev.bandwidth.com/spec/multi-factor-auth.yml", + "resources": None + }, + "phone-number-lookup": { + "url": "https://dev.bandwidth.com/spec/phone-number-lookup.yml", + "resources": None + }, + "insights": { + "url": "https://dev.bandwidth.com/spec/insights.yml", + "resources": None + } +} + +def create_server( + url: str, + username: str = username, + password: str = password, + route_maps: list = None, + resources: list = None +): + """Create an MCP server from the provided spec URL and credentials.""" + spec = requests.get(url).text + spec_object = clean_openapi_spec(yaml.safe_load(spec)) + + auth_bytes = f"{username}:{password}".encode('utf-8') + auth_b64 = base64.b64encode(auth_bytes).decode('utf-8') + + client = httpx.AsyncClient( + base_url=spec_object["servers"][0]["url"], + headers={ + "Authorization": f"Basic {auth_b64}", + } + ) + + mcp = FastMCP.from_openapi( + openapi_spec=spec_object, + client=client, + name="Bandwidth", + route_maps=route_maps, + ) + + if resources: + for resource in resources: + mcp.add_resource(resource) + + return mcp + + +async def create_bandwidth_mcp(mcp: FastMCP, enabled_apis: list | None, excluded_apis: list | None): + """Create the Bandwidth MCP server from all supplied APIs, taking into account enabled and excluded APIs.""" + all_apis = list(api_server_info.keys()) + filtered_apis = filter_apis(all_apis, enabled_apis, excluded_apis) + + for api in filtered_apis: + api_info = api_server_info[api] + server = create_server(api_info["url"], resources=api_info["resources"]) + await mcp.import_server(server) + + return mcp diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..dfee196 --- /dev/null +++ b/utils.py @@ -0,0 +1,130 @@ +import os +import argparse +from dotenv import load_dotenv + +load_dotenv() + +bandwidth_account_id = os.environ["BW_ACCOUNT_ID"] +bandwidth_number = os.environ["BW_NUMBER"] +bandwidth_messaging_application_id = os.environ["BW_MESSAGING_APPLICATION_ID"] +username = os.environ["BW_USERNAME"] +password = os.environ["BW_PASSWORD"].replace("\\", "") + + + +def get_config(): + """Get the Bandwidth configuration""" + return { + "bandwidth_account_id": bandwidth_account_id, + "bandwidth_number": bandwidth_number, + "bandwidth_messaging_application_id": bandwidth_messaging_application_id, + "username": username, + "password": password + } + + +def clean_openapi_spec(spec: dict): + """Recursively clean OpenAPI spec: + - Remove all callbacks + - Remove all 4xx/5xx responses + - Remove any field starting with 'x-' + - Remove all path resources that start with 'x-' + """ + def _clean(obj): + if isinstance(obj, dict): + # Remove 'callbacks' and 'x-' fields + keys_to_remove = [k for k in obj if k == "callbacks" or k.startswith("x-")] + for k in keys_to_remove: + del obj[k] + # Remove 4xx/5xx responses + if "responses" in obj: + codes_to_remove = [code for code in obj["responses"] if code.startswith("4") or code.startswith("5")] + for code in codes_to_remove: + del obj["responses"][code] + # Special handling for paths + if "paths" in obj: + paths_to_remove = [p for p in obj["paths"] if p.startswith("x-")] + for p in paths_to_remove: + del obj["paths"][p] + # Recurse into all values + for v in obj.values(): + _clean(v) + elif isinstance(obj, list): + for item in obj: + _clean(item) + return obj + + return _clean(spec) + + +def parse_cli_args(args=None): + """Parse command line arguments.""" + parser = argparse.ArgumentParser(description="Bandwidth MCP Server") + + # APIs + parser.add_argument( + "--apis", + help="Comma-separated list of API names to enable. If not specified, all tools are enabled.", + type=str, + ) + parser.add_argument( + "--exclude-apis", + help="Comma-separated list of API names to disable.", + type=str, + ) + # parser.add_argument( + # "--list-tools", + # help="List all available tools and exit.", + # action="store_true", + # ) + + return parser.parse_known_args(args)[0] + + +def get_enabled_apis(): + """Get the list of enabled APIs from CLI args or environment variable.""" + try: + args = parse_cli_args() + + if args.apis: + return [api.strip() for api in args.apis.split(",")] + except: + pass + + # Check for environment variable + env_apis = os.getenv("BW_MCP_APIS") + if env_apis: + return [api.strip() for api in env_apis.split(",")] + + return None + + +def get_excluded_apis(): + """Get the list of excluded APIs from CLI args or environment variable.""" + try: + args = parse_cli_args() + + if args.exclude_apis: + return [api.strip() for api in args.exclude_apis.split(",")] + except: + pass + + env_exclude_apis = os.getenv("BW_MCP_EXCLUDE_APIS") + if env_exclude_apis: + return [api.strip() for api in env_exclude_apis.split(",")] + + return [] + + +def filter_apis(all_apis: list, enabled_apis: list | None, excluded_apis: list | None) -> list: + """Return the list of APIs after applying enabled and excluded filters.""" + filtered = all_apis + if enabled_apis: + filtered = [api for api in all_apis if api in enabled_apis] + if not filtered: + raise ValueError("No valid APIs enabled. Please specify at least one valid API to enable.") + if excluded_apis: + filtered = [api for api in all_apis if api not in excluded_apis] + if not filtered: + raise ValueError("All valid APIs excluded. Please specify at least one valid API to include.") + return filtered From 44a5ee1e6e2a1fb6f431f94cc0893ceff1ec0c07 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Thu, 17 Jul 2025 14:58:30 -0400 Subject: [PATCH 02/33] switch from filtering apis to tools and update server --- app.py | 16 ++-- resources.py | 8 +- servers.py | 108 +++++++++++++++----------- utils.py | 210 ++++++++++++++++++++++++++++++++------------------- 4 files changed, 210 insertions(+), 132 deletions(-) diff --git a/app.py b/app.py index b8bfcd9..7281925 100644 --- a/app.py +++ b/app.py @@ -1,25 +1,19 @@ import asyncio from fastmcp import FastMCP - -from utils import get_enabled_apis, get_excluded_apis -from resources import config_resource from servers import create_bandwidth_mcp +from utils import get_enabled_tools, get_excluded_tools, print_server_info mcp = FastMCP(name="Bandwidth MCP") # Initialize the Bandwidth API client async def setup(mcp: FastMCP = mcp): - enabled_apis = get_enabled_apis() - excluded_apis = get_excluded_apis() + enabled_tools = get_enabled_tools() + excluded_tools = get_excluded_tools() - await create_bandwidth_mcp( - mcp, - enabled_apis, - excluded_apis - ) + await create_bandwidth_mcp(mcp, enabled_tools, excluded_tools) + await print_server_info(mcp) -mcp.add_resource(config_resource) if __name__ == "__main__": asyncio.run(setup()) diff --git a/resources.py b/resources.py index 4a1fe82..b67e22f 100644 --- a/resources.py +++ b/resources.py @@ -1,6 +1,6 @@ -from fastmcp.resources import FunctionResource, HttpResource - +from typing import List from utils import get_config +from fastmcp.resources import FunctionResource, HttpResource, Resource config_resource = FunctionResource( name="Bandwidth API Configuration", @@ -19,3 +19,7 @@ mime_type="text/markdown", url="https://dev.bandwidth.com/docs/numbers/guides/searchingForNumbers.md", ) + +def get_bandwidth_resources() -> List[Resource]: + """Get all Bandwidth resources.""" + return [config_resource, number_order_guide_resource] diff --git a/servers.py b/servers.py index f8f8a08..9b3ccca 100644 --- a/servers.py +++ b/servers.py @@ -1,46 +1,48 @@ -import yaml -import httpx -import base64 -import requests from fastmcp import FastMCP -from utils import username, password, clean_openapi_spec, filter_apis -from resources import number_order_guide_resource +from httpx import AsyncClient +from resources import get_bandwidth_resources +from typing import Dict, List, Optional, Callable, Any +from utils import ( + get_config, + create_route_map_fn, + create_auth_header, + fetch_openapi_spec +) -api_server_info = { +api_server_info: Dict[str, Dict[str, Any]] = { "messaging": { - "url": "https://dev.bandwidth.com/spec/messaging.yml", - "resources": [number_order_guide_resource] + "url": "https://dev.bandwidth.com/spec/messaging.yml" }, "multi-factor-auth": { - "url": "https://dev.bandwidth.com/spec/multi-factor-auth.yml", - "resources": None + "url": "https://dev.bandwidth.com/spec/multi-factor-auth.yml" }, "phone-number-lookup": { - "url": "https://dev.bandwidth.com/spec/phone-number-lookup.yml", - "resources": None + "url": "https://dev.bandwidth.com/spec/phone-number-lookup.yml" }, "insights": { - "url": "https://dev.bandwidth.com/spec/insights.yml", - "resources": None + "url": "https://dev.bandwidth.com/spec/insights.yml" } } -def create_server( + +async def _create_server( url: str, - username: str = username, - password: str = password, - route_maps: list = None, - resources: list = None -): + route_map_fn: Optional[Callable] = None +) -> FastMCP: """Create an MCP server from the provided spec URL and credentials.""" - spec = requests.get(url).text - spec_object = clean_openapi_spec(yaml.safe_load(spec)) - - auth_bytes = f"{username}:{password}".encode('utf-8') - auth_b64 = base64.b64encode(auth_bytes).decode('utf-8') + # Fetch and clean the OpenAPI spec + spec_object = await fetch_openapi_spec(url) + + # Validate spec structure + if "servers" not in spec_object or not spec_object["servers"]: + raise ValueError(f"OpenAPI spec from {url} has no servers defined") + + config = get_config() + base_url = spec_object["servers"][0]["url"] + auth_b64 = create_auth_header(config["username"], config["password"]) - client = httpx.AsyncClient( - base_url=spec_object["servers"][0]["url"], + client = AsyncClient( + base_url=base_url, headers={ "Authorization": f"Basic {auth_b64}", } @@ -50,24 +52,46 @@ def create_server( openapi_spec=spec_object, client=client, name="Bandwidth", - route_maps=route_maps, + route_map_fn=route_map_fn, ) - if resources: - for resource in resources: - mcp.add_resource(resource) - return mcp -async def create_bandwidth_mcp(mcp: FastMCP, enabled_apis: list | None, excluded_apis: list | None): - """Create the Bandwidth MCP server from all supplied APIs, taking into account enabled and excluded APIs.""" - all_apis = list(api_server_info.keys()) - filtered_apis = filter_apis(all_apis, enabled_apis, excluded_apis) - - for api in filtered_apis: - api_info = api_server_info[api] - server = create_server(api_info["url"], resources=api_info["resources"]) - await mcp.import_server(server) +async def create_bandwidth_mcp( + mcp: FastMCP, + enabled_tools: Optional[List[str]], + excluded_tools: Optional[List[str]] +) -> FastMCP: + """Create the Bandwidth MCP server from all supplied APIs, taking into account enabled and excluded APIs. + + Args: + mcp: The FastMCP instance to import servers into + enabled_tools: List of tools to enable. If None, all tools are enabled. + excluded_tools: List of tools to exclude. Takes priority over enabled_tools. + + Returns: + The FastMCP instance with all API servers imported + + Raises: + RuntimeError: If any API server fails to create or import + """ + route_map_fn = create_route_map_fn(enabled_tools, excluded_tools) + + for api_name, api_info in api_server_info.items(): + try: + server = await _create_server( + api_info["url"], + route_map_fn=route_map_fn + ) + await mcp.import_server(server) + except Exception as e: + print(f"Warning: Failed to create server for {api_name}: {e}") + for resource in get_bandwidth_resources(): + try: + mcp.add_resource(resource) + except Exception as e: + print(f"Warning: Failed to import resource {resource.name}: {e}") + return mcp diff --git a/utils.py b/utils.py index dfee196..5a986af 100644 --- a/utils.py +++ b/utils.py @@ -1,7 +1,16 @@ import os -import argparse +import copy +import yaml +import httpx +import base64 + +from fastmcp import FastMCP from dotenv import load_dotenv +from argparse import ArgumentParser, Namespace +from fastmcp.server.openapi import MCPType, HTTPRoute +from typing import Dict, List, Optional, Any, Callable +# ===== Config and Server Info ===== load_dotenv() bandwidth_account_id = os.environ["BW_ACCOUNT_ID"] @@ -11,8 +20,7 @@ password = os.environ["BW_PASSWORD"].replace("\\", "") - -def get_config(): +def get_config() -> Dict[str, Any]: """Get the Bandwidth configuration""" return { "bandwidth_account_id": bandwidth_account_id, @@ -23,14 +31,109 @@ def get_config(): } -def clean_openapi_spec(spec: dict): +async def print_server_info(mcp: FastMCP) -> None: + """Print concise server information.""" + try: + all_tools = await mcp.get_tools() + all_resources = await mcp.get_resources() + + tool_names = list(all_tools.keys()) + resource_names = [resource.name for resource in all_resources.values()] + + print("Bandwidth MCP Server Started") + print(f"Tools ({len(tool_names)}): {', '.join(sorted(tool_names)) if tool_names else 'None'}") + print(f"Resources ({len(resource_names)}): {', '.join(sorted(resource_names)) if resource_names else 'None'}") + + except Exception as e: + print(f"Error retrieving server info: {e}") + print("Server may still be functional") + + +# ===== Server Flags ===== +def _parse_cli_args(args: Optional[List[str]] = None) -> Namespace: + """Parse command line arguments with proper type hints.""" + parser = ArgumentParser(description="Bandwidth MCP Server") + + # Tools + parser.add_argument( + "--tools", + help="Comma-separated list of tool names to enable. If not specified, all tools are enabled.", + type=str, + ) + parser.add_argument( + "--exclude-tools", + help="Comma-separated list of tool names to disable.", + type=str, + ) + + return parser.parse_known_args(args)[0] + + +def _parse_arg_list(arg_string: str) -> List[str]: + """Parse a comma-separated argument string into a list.""" + return [item.strip() for item in arg_string.split(",") if item.strip()] + + +def _parse_flags(cli_arg: Optional[str], env_var: str) -> Optional[List[str]]: + """Get flag values from CLI argument or environment variable.""" + # Try CLI argument first + if cli_arg: + return _parse_arg_list(cli_arg) + + # Fall back to environment variable + env_value = os.getenv(env_var) + if env_value: + return _parse_arg_list(env_value) + + return None + + +# ===== Tool Management ===== +def get_enabled_tools() -> Optional[List[str]]: + """Get the list of enabled tools from CLI args or environment variable.""" + args = _parse_cli_args() + return _parse_flags(args.tools, "BW_MCP_TOOLS") + + +def get_excluded_tools() -> Optional[List[str]]: + """Get the list of excluded tools from CLI args or environment variable.""" + args = _parse_cli_args() + return _parse_flags(args.exclude_tools, "BW_MCP_EXCLUDE_TOOLS") + + +def create_route_map_fn(enabled_tools: Optional[List[str]], excluded_tools: Optional[List[str]]) -> Callable[[HTTPRoute, MCPType], MCPType]: + """Create a route map function based on enabled and excluded tools. + + Args: + enabled_tools: List of tools to enable. If None, all tools are enabled. + excluded_tools: List of tools to exclude. Takes priority over enabled_tools. + + Returns: + A function that maps routes to MCP types based on the tool configuration. + """ + def route_map_fn(route: HTTPRoute, mcp_type: MCPType) -> MCPType: + # Excluded tools have priority - if provided, ignore enabled tools + if excluded_tools: + return mcp_type if route.operation_id not in excluded_tools else MCPType.EXCLUDE + if enabled_tools: + return mcp_type if route.operation_id in enabled_tools else MCPType.EXCLUDE + + return mcp_type + + return route_map_fn + + +# ===== OpenAPI Spec Operations ===== +def _clean_openapi_spec(spec: Dict[str, Any]) -> Dict[str, Any]: """Recursively clean OpenAPI spec: - Remove all callbacks - Remove all 4xx/5xx responses - Remove any field starting with 'x-' - Remove all path resources that start with 'x-' """ - def _clean(obj): + cleaned_spec = copy.deepcopy(spec) + + def _clean(obj: Any) -> Any: if isinstance(obj, dict): # Remove 'callbacks' and 'x-' fields keys_to_remove = [k for k in obj if k == "callbacks" or k.startswith("x-")] @@ -38,7 +141,8 @@ def _clean(obj): del obj[k] # Remove 4xx/5xx responses if "responses" in obj: - codes_to_remove = [code for code in obj["responses"] if code.startswith("4") or code.startswith("5")] + codes_to_remove = [code for code in obj["responses"] + if str(code).startswith(("4", "5"))] for code in codes_to_remove: del obj["responses"][code] # Special handling for paths @@ -54,77 +158,29 @@ def _clean(obj): _clean(item) return obj - return _clean(spec) - - -def parse_cli_args(args=None): - """Parse command line arguments.""" - parser = argparse.ArgumentParser(description="Bandwidth MCP Server") - - # APIs - parser.add_argument( - "--apis", - help="Comma-separated list of API names to enable. If not specified, all tools are enabled.", - type=str, - ) - parser.add_argument( - "--exclude-apis", - help="Comma-separated list of API names to disable.", - type=str, - ) - # parser.add_argument( - # "--list-tools", - # help="List all available tools and exit.", - # action="store_true", - # ) - - return parser.parse_known_args(args)[0] - - -def get_enabled_apis(): - """Get the list of enabled APIs from CLI args or environment variable.""" - try: - args = parse_cli_args() - - if args.apis: - return [api.strip() for api in args.apis.split(",")] - except: - pass - - # Check for environment variable - env_apis = os.getenv("BW_MCP_APIS") - if env_apis: - return [api.strip() for api in env_apis.split(",")] - - return None + return _clean(cleaned_spec) -def get_excluded_apis(): - """Get the list of excluded APIs from CLI args or environment variable.""" +async def fetch_openapi_spec(url: str) -> Dict[str, Any]: + """Fetch and parse OpenAPI spec from URL.""" try: - args = parse_cli_args() - - if args.exclude_apis: - return [api.strip() for api in args.exclude_apis.split(",")] - except: - pass - - env_exclude_apis = os.getenv("BW_MCP_EXCLUDE_APIS") - if env_exclude_apis: - return [api.strip() for api in env_exclude_apis.split(",")] - - return [] - - -def filter_apis(all_apis: list, enabled_apis: list | None, excluded_apis: list | None) -> list: - """Return the list of APIs after applying enabled and excluded filters.""" - filtered = all_apis - if enabled_apis: - filtered = [api for api in all_apis if api in enabled_apis] - if not filtered: - raise ValueError("No valid APIs enabled. Please specify at least one valid API to enable.") - if excluded_apis: - filtered = [api for api in all_apis if api not in excluded_apis] - if not filtered: - raise ValueError("All valid APIs excluded. Please specify at least one valid API to include.") - return filtered + async with httpx.AsyncClient() as client: + response = await client.get(url) + response.raise_for_status() + spec_text = response.text + + spec_object = yaml.safe_load(spec_text) + if not spec_object: + raise ValueError(f"Empty or invalid YAML spec from {url}") + + return _clean_openapi_spec(spec_object) + except httpx.HTTPError as e: + raise RuntimeError(f"Failed to fetch OpenAPI spec from {url}: {e}") from e + except yaml.YAMLError as e: + raise RuntimeError(f"Failed to parse YAML spec from {url}: {e}") from e + + +def create_auth_header(username: str, password: str) -> str: + """Create a basic authentication header.""" + auth_bytes = f"{username}:{password}".encode('utf-8') + return base64.b64encode(auth_bytes).decode('utf-8') From 97bcf3e9d18e1d824ed2360369c1827f21518710 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Wed, 6 Aug 2025 17:28:24 -0400 Subject: [PATCH 03/33] move for `uv` and update readme --- README.md | 161 +++++++++++++++++++++++++------ inspect.sh | 4 - pyproject.toml | 18 ++++ requirements.txt | 46 +-------- app.py => src/app.py | 8 +- resources.py => src/resources.py | 0 servers.py => src/servers.py | 0 utils.py => src/utils.py | 8 +- 8 files changed, 167 insertions(+), 78 deletions(-) delete mode 100644 inspect.sh create mode 100644 pyproject.toml rename app.py => src/app.py (80%) rename resources.py => src/resources.py (100%) rename servers.py => src/servers.py (100%) rename utils.py => src/utils.py (94%) diff --git a/README.md b/README.md index 11f13b4..e47ee44 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,10 @@ # Bandwidth Official MCP Server -Source code for the official Bandwidth Model Context Protocol (MCP) Server. This server can be used to interact with different Bandwidth APIs via an AI agent. The server is provided as a PyPi package but may also be cloned directly from this repo. +Source code for the official Bandwidth Model Context Protocol (MCP) Server. +This server can be used to interact with different Bandwidth APIs via an AI agent. +The server is provided as a python package and may be cloned directly from this repo. ## Installation -### Install from PyPi - - -### Install Locally - Clone directly from this git repository using: ```shell @@ -15,20 +12,137 @@ git clone https://github.com/Bandwidth/bandwidth-mcp-server.git cd bandwidth-mcp-server ``` -## Prerequisites +## Getting Started + +### Prerequisites In order to use the Bandwidth MCP Server, you'll need the following things, set as environment variables. - Valid Bandwidth API Credentials + - This will be the username and password of your Bandwidth API user - For more info on creating API credentials, see our [Credentials](https://dev.bandwidth.com/docs/credentials) page - Bandwidth Account ID + - The ID of the account you'd like to make API calls on behalf of -Conditional Variables: -- Messaging Application ID - - Necessary when using our Messaging API -- Bandwidth Telephone Number - - +### Configuration -## Getting Started +#### Environment Variables + +Environment variables can be set one of three ways for usage with the Bandwidth MCP Server: + +1. System environment variables. +2. `.env` file - The package includes the `python-dotenv` package to allow reading from dotenv files. +3. Configured with your AI agent of choice - See our usage guides below. + +The following variables will be required to use the server: + +```sh +BW_ACCOUNT_ID # Your Bandwidth Account ID +BW_USERNAME # Your Bandwidth API User Username +BW_PASSWORD # Your Bandwidth API User Password +``` + +The following variables are optional or conditionally required: + +```sh +BW_NUMBER # A valid phone number on your Bandwidth account. Used with our Messaging and MFA APIs. +BW_MESSAGING_APPLICATION_ID # A Bandwidth Messaging Application ID. Used with our Messaging and MFA APIs. +BW_VOICE_APPLICATION_ID # A Bandwidth Voice Application ID. Used with our MFA API. +BW_MCP_TOOLS # The list of MCP tools you'd like to enable. If not set, all tools are enabled. +BW_MCP_EXCLUDE_TOOLS # The list of MCP tools you'd like to exclude. Takes priority over BW_MCP_TOOLS. +``` + +#### Including or Excluding Tools + +By default, the server provides and enables all tools listed in the [Tools List](#tools-list). +Enabling all of these tools may cause context window size issues for certain AI agents or lead to slower agent response times. +To work around this and for a better experience, we recommend enabling only the certain subset of tools you plan on using. + +This can be accomplished by supplying a list of tool names to specifically enable or exclude to the server. +This list must be comma separated, with the tool names matching their names in the [Tools List](#tools-list). +The `BW_MCP_TOOLS` and `BW_MCP_EXCLUDE_TOOLS` mentioned in the [Environment Variables](#environment-variables) +section allow for enabling and excluding tools by name. You can also use the CLI flags `--tools` and `--exclude-tools`. +Using the CLI flags will take priority over the environment variables, and providing tools to exclude will take priority over the list of enabled tools. + +##### Tool Filtering Examples + +**Including only our Messaging tools** + +```sh +# Environment Variable +BW_MCP_TOOLS=listMessages,createMessage,createMultiChannelMessage + +# CLI Flag +--tools listMessages,createMessage,createMultiChannelMessage +``` + +**Excluding our Phone Number Lookup Tools** + +```sh +# Environment Variable +BW_MCP_EXCLUDE_TOOLS=createLookup,getLookupStatus + +# CLI Flag +--exclude-tools createLookup,getLookupStatus +``` + +## Using the Server + +Below you'll find instructions for using our MCP server with different common AI agents, as well as instructions for running the server locally. For usage with AI agents, it is recommended to use a combination of [uv](https://github.com/astral-sh/uv?tab=readme-ov-file#uv) and environment variables to start and configure the server respectively. + +### Claude Desktop + +1. Install [Claude Desktop](https://claude.ai/download) +2. Edit your `claude_desktop_config.json` to include the following object + +```json +{ + "mcpServers": { + "Bandwidth": { + "command": "uvx", + "args": ["--from", "/path/to/bandwidth-mcp-server", "start"], + "env": { + "BW_USERNAME": "", + "BW_PASSWORD": "", + "BW_ACCOUNT_ID": "", + "BW_MCP_TOOLS": "tools,to,enable", + "BW_MCP_EXCLUDE_TOOLS": "tools,to,exclude", + } + } + } +} +``` + +> **_NOTE:_** You can also run the server directly from our github repo by replacing +`/path/to/bandwidth-mcp-server` with: `git+https://github.com/Bandwidth/bandwidth-mcp-server.git` + +### Goose CLI + +1. Install [Goose CLI](https://block.github.io/goose/docs/getting-started/installation/) +2. Add the Bandwidth MCP Server as an Extension + +```shell +goose configure +``` + +Then follow the prompts like the example below, making sure to add all of the relevant environment variables at the end. + +```shell +┌ goose-configure +│ +◇ What would you like to configure? +│ Add Extension +│ +◇ What type of extension would you like to add? +│ Command-line Extension +│ +◇ What would you like to call this extension? +│ bw-mcp-server +│ +◇ What command should be run? +│ uvx --from /path/to/bandwidth-mcp-server start +``` + +### Cursor ### Running the Server Standalone @@ -53,26 +167,19 @@ pip install -r requirements.txt After all packages are installed in the virtual environment, you can run the server locally using: ```sh -python app.py +python src/app.py ``` #### Run Using uv -### Usage with Claude - - -### Usage with goose - +Make sure you have [uv installed](https://github.com/astral-sh/uv?tab=readme-ov-file#installation), +then you can start the server by running the following command from the root directory of this repository. -### Configuration - -### Environment Variables - -#### Including or Excluding APIs - -#### Including or Excluding Workflows +```sh +uvx --from ./ start +``` -## List of all Tools Supplied by the Server +## Tools List ## **Multi-Factor Authentication (MFA)** - `bw-mcp-server__generateMessagingCode` - Send MFA code via SMS diff --git a/inspect.sh b/inspect.sh deleted file mode 100644 index 72b284e..0000000 --- a/inspect.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -npx @modelcontextprotocol/inspector \ - /Users/ckoegel/Documents/repositories/misc/mcp-poc/.venv/bin/python /Users/ckoegel/Documents/repositories/misc/mcp-poc/app.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..14555f9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "bw-mcp-server" +version = "0.1.0" +description = "Bandwidth MCP Server" +authors = [ + {name = "Bandwidth", email = "dx@bandwidth.com"} +] +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "python-dotenv~=1.1.0", + "httpx~=0.28.0", + "fastmcp~=2.11.0", + "pyyaml~=6.0.0" +] + +[project.scripts] +start = "app:main" diff --git a/requirements.txt b/requirements.txt index 14a2dc8..3b71427 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,42 +1,4 @@ -annotated-types==0.7.0 -anyio==4.9.0 -Authlib==1.6.0 -bandwidth-sdk==19.1.0 -certifi==2025.6.15 -cffi==1.17.1 -charset-normalizer==3.4.2 -click==8.2.1 -cryptography==45.0.4 -exceptiongroup==1.3.0 -fastmcp==2.9.0 -h11==0.16.0 -httpcore==1.0.9 -httpx==0.28.1 -httpx-sse==0.4.1 -idna==3.10 -load-dotenv==0.1.0 -markdown-it-py==3.0.0 -mcp==1.9.4 -mdurl==0.1.2 -openapi-pydantic==0.5.1 -pycparser==2.22 -pydantic==2.11.7 -pydantic-settings==2.10.1 -pydantic_core==2.33.2 -Pygments==2.19.2 -python-dateutil==2.9.0.post0 -python-dotenv==1.1.1 -python-multipart==0.0.20 -PyYAML==6.0.2 -requests==2.32.4 -rich==14.0.0 -shellingham==1.5.4 -six==1.17.0 -sniffio==1.3.1 -sse-starlette==2.3.6 -starlette==0.47.1 -typer==0.16.0 -typing-inspection==0.4.1 -typing_extensions==4.14.0 -urllib3==2.5.0 -uvicorn==0.34.3 +python-dotenv~=1.1.0 +httpx~=0.28.0 +fastmcp~=2.11.0 +pyyaml~=6.0.0 diff --git a/app.py b/src/app.py similarity index 80% rename from app.py rename to src/app.py index 7281925..5cbf0e1 100644 --- a/app.py +++ b/src/app.py @@ -11,10 +11,14 @@ async def setup(mcp: FastMCP = mcp): enabled_tools = get_enabled_tools() excluded_tools = get_excluded_tools() + print("Setting up Bandwidth MCP server...") await create_bandwidth_mcp(mcp, enabled_tools, excluded_tools) await print_server_info(mcp) - -if __name__ == "__main__": +def main(): + """Main function to run the Bandwidth MCP server.""" asyncio.run(setup()) mcp.run() + +if __name__ == "__main__": + main() diff --git a/resources.py b/src/resources.py similarity index 100% rename from resources.py rename to src/resources.py diff --git a/servers.py b/src/servers.py similarity index 100% rename from servers.py rename to src/servers.py diff --git a/utils.py b/src/utils.py similarity index 94% rename from utils.py rename to src/utils.py index 5a986af..8fe5618 100644 --- a/utils.py +++ b/src/utils.py @@ -13,9 +13,10 @@ # ===== Config and Server Info ===== load_dotenv() -bandwidth_account_id = os.environ["BW_ACCOUNT_ID"] -bandwidth_number = os.environ["BW_NUMBER"] -bandwidth_messaging_application_id = os.environ["BW_MESSAGING_APPLICATION_ID"] +bandwidth_account_id = os.environ.get("BW_ACCOUNT_ID", None) +bandwidth_number = os.environ.get("BW_NUMBER", None) +bandwidth_messaging_application_id = os.environ.get("BW_MESSAGING_APPLICATION_ID", None) +bandwidth_voice_application_id = os.environ.get("BW_VOICE_APPLICATION_ID", None) username = os.environ["BW_USERNAME"] password = os.environ["BW_PASSWORD"].replace("\\", "") @@ -26,6 +27,7 @@ def get_config() -> Dict[str, Any]: "bandwidth_account_id": bandwidth_account_id, "bandwidth_number": bandwidth_number, "bandwidth_messaging_application_id": bandwidth_messaging_application_id, + "bandwidth_voice_application_id": bandwidth_voice_application_id, "username": username, "password": password } From 3a95fbe4aba1d55b8ff71a43e3f6e030f252d74a Mon Sep 17 00:00:00 2001 From: ckoegel Date: Thu, 7 Aug 2025 17:15:12 -0400 Subject: [PATCH 04/33] refactor --- README.md | 54 +++++++-------- pyproject.toml | 1 - requirements.txt | 1 - src/app.py | 12 ++-- src/config.py | 74 ++++++++++++++++++++ src/resources.py | 14 +--- src/{utils.py => server_utils.py} | 109 +++++++++--------------------- src/servers.py | 29 ++++---- 8 files changed, 156 insertions(+), 138 deletions(-) create mode 100644 src/config.py rename src/{utils.py => server_utils.py} (59%) diff --git a/README.md b/README.md index e47ee44..9319257 100644 --- a/README.md +++ b/README.md @@ -27,16 +27,12 @@ In order to use the Bandwidth MCP Server, you'll need the following things, set #### Environment Variables -Environment variables can be set one of three ways for usage with the Bandwidth MCP Server: - -1. System environment variables. -2. `.env` file - The package includes the `python-dotenv` package to allow reading from dotenv files. -3. Configured with your AI agent of choice - See our usage guides below. +Environment variables are used to configure the Bandwidth MCP Server. +The server will respect both system environment variables and those configured via your AI agent. The following variables will be required to use the server: ```sh -BW_ACCOUNT_ID # Your Bandwidth Account ID BW_USERNAME # Your Bandwidth API User Username BW_PASSWORD # Your Bandwidth API User Password ``` @@ -44,6 +40,7 @@ BW_PASSWORD # Your Bandwidth API User Password The following variables are optional or conditionally required: ```sh +BW_ACCOUNT_ID # Your Bandwidth Account ID. Required for most API operations. BW_NUMBER # A valid phone number on your Bandwidth account. Used with our Messaging and MFA APIs. BW_MESSAGING_APPLICATION_ID # A Bandwidth Messaging Application ID. Used with our Messaging and MFA APIs. BW_VOICE_APPLICATION_ID # A Bandwidth Voice Application ID. Used with our MFA API. @@ -103,7 +100,6 @@ Below you'll find instructions for using our MCP server with different common AI "env": { "BW_USERNAME": "", "BW_PASSWORD": "", - "BW_ACCOUNT_ID": "", "BW_MCP_TOOLS": "tools,to,enable", "BW_MCP_EXCLUDE_TOOLS": "tools,to,exclude", } @@ -118,13 +114,13 @@ Below you'll find instructions for using our MCP server with different common AI ### Goose CLI 1. Install [Goose CLI](https://block.github.io/goose/docs/getting-started/installation/) -2. Add the Bandwidth MCP Server as an Extension +2. Add the Bandwidth MCP Server as a Command-line Extension ```shell goose configure ``` -Then follow the prompts like the example below, making sure to add all of the relevant environment variables at the end. +Then follow the prompts like the example below. ```shell ┌ goose-configure @@ -142,6 +138,8 @@ Then follow the prompts like the example below, making sure to add all of the re │ uvx --from /path/to/bandwidth-mcp-server start ``` +> **_NOTE:_** If you configure environment variables with Goose, it will prioritize those over your system environment variables. + ### Cursor ### Running the Server Standalone @@ -182,32 +180,32 @@ uvx --from ./ start ## Tools List ## **Multi-Factor Authentication (MFA)** -- `bw-mcp-server__generateMessagingCode` - Send MFA code via SMS -- `bw-mcp-server__generateVoiceCode` - Send MFA code via voice call -- `bw-mcp-server__verifyCode` - Verify a previously sent MFA code +- `generateMessagingCode` - Send MFA code via SMS +- `generateVoiceCode` - Send MFA code via voice call +- `verifyCode` - Verify a previously sent MFA code ## **Phone Number Lookup** -- `bw-mcp-server__createLookup` - Create a phone number lookup request -- `bw-mcp-server__getLookupStatus` - Get status of an existing lookup request +- `createLookup` - Create a phone number lookup request +- `getLookupStatus` - Get status of an existing lookup request ## **Voice & Call Management** -- `bw-mcp-server__listCalls` - Returns a list of call events with filtering options -- `bw-mcp-server__listCall` - Returns details for a single call event +- `listCalls` - Returns a list of call events with filtering options +- `listCall` - Returns details for a single call event ## **Reporting & Analytics** -- `bw-mcp-server__getReports` - Get history of created reports -- `bw-mcp-server__createReport` - Create a new report instance -- `bw-mcp-server__getReportStatus` - Get status of a report -- `bw-mcp-server__getReportFile` - Download report file -- `bw-mcp-server__getReportDefinitions` - Get available report definitions +- `getReports` - Get history of created reports +- `createReport` - Create a new report instance +- `getReportStatus` - Get status of a report +- `getReportFile` - Download report file +- `getReportDefinitions` - Get available report definitions ## **Media Management** -- `bw-mcp-server__listMedia` - List your media files -- `bw-mcp-server__getMedia` - Download a specific media file -- `bw-mcp-server__uploadMedia` - Upload a media file -- `bw-mcp-server__deleteMedia` - Delete a media file +- `listMedia` - List your media files +- `getMedia` - Download a specific media file +- `uploadMedia` - Upload a media file +- `deleteMedia` - Delete a media file ## **Messaging** -- `bw-mcp-server__listMessages` - List messages with filtering options -- `bw-mcp-server__createMessage` - Send SMS/MMS messages -- `bw-mcp-server__createMultiChannelMessage` - Send multi-channel messages (RBM, SMS, MMS) +- `listMessages` - List messages with filtering options +- `createMessage` - Send SMS/MMS messages +- `createMultiChannelMessage` - Send multi-channel messages (RBM, SMS, MMS) diff --git a/pyproject.toml b/pyproject.toml index 14555f9..c4131b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,6 @@ authors = [ readme = "README.md" requires-python = ">=3.10" dependencies = [ - "python-dotenv~=1.1.0", "httpx~=0.28.0", "fastmcp~=2.11.0", "pyyaml~=6.0.0" diff --git a/requirements.txt b/requirements.txt index 3b71427..4adbbdd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -python-dotenv~=1.1.0 httpx~=0.28.0 fastmcp~=2.11.0 pyyaml~=6.0.0 diff --git a/src/app.py b/src/app.py index 5cbf0e1..fc68c6d 100644 --- a/src/app.py +++ b/src/app.py @@ -2,18 +2,22 @@ from fastmcp import FastMCP from servers import create_bandwidth_mcp -from utils import get_enabled_tools, get_excluded_tools, print_server_info +from config import ( + load_config, + get_enabled_tools, + get_excluded_tools +) mcp = FastMCP(name="Bandwidth MCP") -# Initialize the Bandwidth API client async def setup(mcp: FastMCP = mcp): + """Setup the Bandwidth MCP server with tools and resources.""" enabled_tools = get_enabled_tools() excluded_tools = get_excluded_tools() + config = load_config() print("Setting up Bandwidth MCP server...") - await create_bandwidth_mcp(mcp, enabled_tools, excluded_tools) - await print_server_info(mcp) + await create_bandwidth_mcp(mcp, enabled_tools, excluded_tools, config) def main(): """Main function to run the Bandwidth MCP server.""" diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..d9f6894 --- /dev/null +++ b/src/config.py @@ -0,0 +1,74 @@ +import os + +from typing import Dict, List, Optional +from argparse import ArgumentParser, Namespace + + +def load_config() -> Dict[str, str]: + """Load Bandwidth configuration from environment variables.""" + config = {} + required_vars = ["BW_USERNAME", "BW_PASSWORD"] + optional_vars = ["BW_ACCOUNT_ID", "BW_NUMBER", "BW_MESSAGING_APPLICATION_ID", "BW_VOICE_APPLICATION_ID"] + + # Required variables + for var in required_vars: + if var not in os.environ: + raise ValueError(f"Missing required environment variable: {var}") + + # Add all variables that exist + for var in required_vars + optional_vars: + value = os.getenv(var) + if value: + config[var] = value + + return config + + +def _parse_cli_args(args: Optional[List[str]] = None) -> Namespace: + """Parse command line arguments with proper type hints.""" + parser = ArgumentParser(description="Bandwidth MCP Server") + + # Tools + parser.add_argument( + "--tools", + help="Comma-separated list of tool names to enable. If not specified, all tools are enabled.", + type=str, + ) + parser.add_argument( + "--exclude-tools", + help="Comma-separated list of tool names to disable.", + type=str, + ) + + return parser.parse_known_args(args)[0] + + +def _parse_arg_list(arg_string: str) -> List[str]: + """Parse a comma-separated argument string into a list.""" + return [item.strip() for item in arg_string.split(",") if item.strip()] + + +def _parse_flags(cli_arg: Optional[str], env_var: str) -> Optional[List[str]]: + """Get flag values from CLI argument or environment variable.""" + # Try CLI argument first + if cli_arg: + return _parse_arg_list(cli_arg) + + # Fall back to environment variable + env_value = os.getenv(env_var) + if env_value: + return _parse_arg_list(env_value) + + return None + + +def get_enabled_tools() -> Optional[List[str]]: + """Get the list of enabled tools from CLI args or environment variable.""" + args = _parse_cli_args() + return _parse_flags(args.tools, "BW_MCP_TOOLS") + + +def get_excluded_tools() -> Optional[List[str]]: + """Get the list of excluded tools from CLI args or environment variable.""" + args = _parse_cli_args() + return _parse_flags(args.exclude_tools, "BW_MCP_EXCLUDE_TOOLS") diff --git a/src/resources.py b/src/resources.py index b67e22f..289f5e1 100644 --- a/src/resources.py +++ b/src/resources.py @@ -1,15 +1,5 @@ from typing import List -from utils import get_config -from fastmcp.resources import FunctionResource, HttpResource, Resource - -config_resource = FunctionResource( - name="Bandwidth API Configuration", - description="Configuration Object for Bandwidth API", - tags={"bandwidth", "config"}, - uri="data://config", - mime_type="application/json", - fn=get_config, -) +from fastmcp.resources import HttpResource, Resource number_order_guide_resource = HttpResource( name="Bandwidth Number Order Guide", @@ -22,4 +12,4 @@ def get_bandwidth_resources() -> List[Resource]: """Get all Bandwidth resources.""" - return [config_resource, number_order_guide_resource] + return [number_order_guide_resource] diff --git a/src/utils.py b/src/server_utils.py similarity index 59% rename from src/utils.py rename to src/server_utils.py index 8fe5618..4d34095 100644 --- a/src/utils.py +++ b/src/server_utils.py @@ -1,37 +1,14 @@ -import os import copy import yaml import httpx import base64 from fastmcp import FastMCP -from dotenv import load_dotenv -from argparse import ArgumentParser, Namespace +from resources import get_bandwidth_resources +from fastmcp.resources import FunctionResource from fastmcp.server.openapi import MCPType, HTTPRoute from typing import Dict, List, Optional, Any, Callable -# ===== Config and Server Info ===== -load_dotenv() - -bandwidth_account_id = os.environ.get("BW_ACCOUNT_ID", None) -bandwidth_number = os.environ.get("BW_NUMBER", None) -bandwidth_messaging_application_id = os.environ.get("BW_MESSAGING_APPLICATION_ID", None) -bandwidth_voice_application_id = os.environ.get("BW_VOICE_APPLICATION_ID", None) -username = os.environ["BW_USERNAME"] -password = os.environ["BW_PASSWORD"].replace("\\", "") - - -def get_config() -> Dict[str, Any]: - """Get the Bandwidth configuration""" - return { - "bandwidth_account_id": bandwidth_account_id, - "bandwidth_number": bandwidth_number, - "bandwidth_messaging_application_id": bandwidth_messaging_application_id, - "bandwidth_voice_application_id": bandwidth_voice_application_id, - "username": username, - "password": password - } - async def print_server_info(mcp: FastMCP) -> None: """Print concise server information.""" @@ -51,59 +28,11 @@ async def print_server_info(mcp: FastMCP) -> None: print("Server may still be functional") -# ===== Server Flags ===== -def _parse_cli_args(args: Optional[List[str]] = None) -> Namespace: - """Parse command line arguments with proper type hints.""" - parser = ArgumentParser(description="Bandwidth MCP Server") - - # Tools - parser.add_argument( - "--tools", - help="Comma-separated list of tool names to enable. If not specified, all tools are enabled.", - type=str, - ) - parser.add_argument( - "--exclude-tools", - help="Comma-separated list of tool names to disable.", - type=str, - ) - - return parser.parse_known_args(args)[0] - - -def _parse_arg_list(arg_string: str) -> List[str]: - """Parse a comma-separated argument string into a list.""" - return [item.strip() for item in arg_string.split(",") if item.strip()] - - -def _parse_flags(cli_arg: Optional[str], env_var: str) -> Optional[List[str]]: - """Get flag values from CLI argument or environment variable.""" - # Try CLI argument first - if cli_arg: - return _parse_arg_list(cli_arg) - - # Fall back to environment variable - env_value = os.getenv(env_var) - if env_value: - return _parse_arg_list(env_value) - - return None - -# ===== Tool Management ===== -def get_enabled_tools() -> Optional[List[str]]: - """Get the list of enabled tools from CLI args or environment variable.""" - args = _parse_cli_args() - return _parse_flags(args.tools, "BW_MCP_TOOLS") - - -def get_excluded_tools() -> Optional[List[str]]: - """Get the list of excluded tools from CLI args or environment variable.""" - args = _parse_cli_args() - return _parse_flags(args.exclude_tools, "BW_MCP_EXCLUDE_TOOLS") - - -def create_route_map_fn(enabled_tools: Optional[List[str]], excluded_tools: Optional[List[str]]) -> Callable[[HTTPRoute, MCPType], MCPType]: +def create_route_map_fn( + enabled_tools: Optional[List[str]], + excluded_tools: Optional[List[str]] +) -> Callable[[HTTPRoute, MCPType], MCPType]: """Create a route map function based on enabled and excluded tools. Args: @@ -125,7 +54,6 @@ def route_map_fn(route: HTTPRoute, mcp_type: MCPType) -> MCPType: return route_map_fn -# ===== OpenAPI Spec Operations ===== def _clean_openapi_spec(spec: Dict[str, Any]) -> Dict[str, Any]: """Recursively clean OpenAPI spec: - Remove all callbacks @@ -186,3 +114,28 @@ def create_auth_header(username: str, password: str) -> str: """Create a basic authentication header.""" auth_bytes = f"{username}:{password}".encode('utf-8') return base64.b64encode(auth_bytes).decode('utf-8') + + +def add_resources(mcp: FastMCP, config: Dict[str, Any]) -> FastMCP: + """Add configuration and other resources to the MCP server.""" + config_resource = FunctionResource( + name="Bandwidth API Configuration", + description="Configuration Object for Bandwidth API", + tags={"bandwidth", "config"}, + uri="data://config", + mime_type="application/json", + fn=lambda: config + ) + + try: + mcp.add_resource(config_resource) + except Exception as e: + print(f"Warning: Failed to add config resource: {e}") + + for resource in get_bandwidth_resources(): + try: + mcp.add_resource(resource) + except Exception as e: + print(f"Warning: Failed to import resource {resource.name}: {e}") + + return mcp diff --git a/src/servers.py b/src/servers.py index 9b3ccca..05d4760 100644 --- a/src/servers.py +++ b/src/servers.py @@ -1,12 +1,12 @@ from fastmcp import FastMCP from httpx import AsyncClient -from resources import get_bandwidth_resources from typing import Dict, List, Optional, Callable, Any -from utils import ( - get_config, +from server_utils import ( + add_resources, create_route_map_fn, create_auth_header, - fetch_openapi_spec + fetch_openapi_spec, + print_server_info ) api_server_info: Dict[str, Dict[str, Any]] = { @@ -27,7 +27,8 @@ async def _create_server( url: str, - route_map_fn: Optional[Callable] = None + route_map_fn: Optional[Callable] = None, + config: Dict[str, Any] = {} ) -> FastMCP: """Create an MCP server from the provided spec URL and credentials.""" # Fetch and clean the OpenAPI spec @@ -37,14 +38,14 @@ async def _create_server( if "servers" not in spec_object or not spec_object["servers"]: raise ValueError(f"OpenAPI spec from {url} has no servers defined") - config = get_config() base_url = spec_object["servers"][0]["url"] - auth_b64 = create_auth_header(config["username"], config["password"]) + auth_b64 = create_auth_header(config["BW_USERNAME"], config["BW_PASSWORD"]) client = AsyncClient( base_url=base_url, headers={ "Authorization": f"Basic {auth_b64}", + "User-Agent": "Bandwidth MCP Server" } ) @@ -61,7 +62,8 @@ async def _create_server( async def create_bandwidth_mcp( mcp: FastMCP, enabled_tools: Optional[List[str]], - excluded_tools: Optional[List[str]] + excluded_tools: Optional[List[str]], + config: Dict[str, Any] = {} ) -> FastMCP: """Create the Bandwidth MCP server from all supplied APIs, taking into account enabled and excluded APIs. @@ -69,6 +71,7 @@ async def create_bandwidth_mcp( mcp: The FastMCP instance to import servers into enabled_tools: List of tools to enable. If None, all tools are enabled. excluded_tools: List of tools to exclude. Takes priority over enabled_tools. + config: Configuration dictionary containing API credentials and other variables. Returns: The FastMCP instance with all API servers imported @@ -82,16 +85,14 @@ async def create_bandwidth_mcp( try: server = await _create_server( api_info["url"], - route_map_fn=route_map_fn + route_map_fn=route_map_fn, + config=config ) await mcp.import_server(server) except Exception as e: print(f"Warning: Failed to create server for {api_name}: {e}") - for resource in get_bandwidth_resources(): - try: - mcp.add_resource(resource) - except Exception as e: - print(f"Warning: Failed to import resource {resource.name}: {e}") + add_resources(mcp, config) + await print_server_info(mcp) return mcp From 57b8d7929d1890366f1e315e56da61167dafa0d7 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Fri, 8 Aug 2025 11:46:19 -0400 Subject: [PATCH 05/33] cleanup --- README.md | 20 ++++++++++++++++++++ src/resources.py | 3 ++- src/server_utils.py | 8 ++++---- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9319257..a7941a2 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,26 @@ Then follow the prompts like the example below. ### Cursor +1. Install [Cursor](https://cursor.com/downloads) +2. Update your `.cursor/mcp.json` file to include the following object + +```json +{ + "mcpServers": { + "bw-mcp-server": { + "command": "/Users/ckoegel/Documents/repositories/sdks/bandwidth-mcp-server/.venv/bin/python", + "args": ["/Users/ckoegel/Documents/repositories/sdks/bandwidth-mcp-server/src/app.py"], + "env": { + "BW_USERNAME": "", + "BW_PASSWORD": "", + "BW_MCP_TOOLS": "tools,to,enable", + "BW_MCP_EXCLUDE_TOOLS": "tools,to,exclude", + } + } + } +} +``` + ### Running the Server Standalone The MCP server can be run locally using either native python or uv. diff --git a/src/resources.py b/src/resources.py index 289f5e1..3d0710d 100644 --- a/src/resources.py +++ b/src/resources.py @@ -5,11 +5,12 @@ name="Bandwidth Number Order Guide", description="Bandwidth Number Order Guide", tags={"bandwidth", "number", "order", "guide"}, - uri="data://number_order_guide", + uri="resource://number_order_guide", mime_type="text/markdown", url="https://dev.bandwidth.com/docs/numbers/guides/searchingForNumbers.md", ) + def get_bandwidth_resources() -> List[Resource]: """Get all Bandwidth resources.""" return [number_order_guide_resource] diff --git a/src/server_utils.py b/src/server_utils.py index 4d34095..250775b 100644 --- a/src/server_utils.py +++ b/src/server_utils.py @@ -28,7 +28,6 @@ async def print_server_info(mcp: FastMCP) -> None: print("Server may still be functional") - def create_route_map_fn( enabled_tools: Optional[List[str]], excluded_tools: Optional[List[str]] @@ -120,14 +119,15 @@ def add_resources(mcp: FastMCP, config: Dict[str, Any]) -> FastMCP: """Add configuration and other resources to the MCP server.""" config_resource = FunctionResource( name="Bandwidth API Configuration", - description="Configuration Object for Bandwidth API", - tags={"bandwidth", "config"}, - uri="data://config", + description="Object containing API credentials, application IDs, and account ID.", + tags={"bandwidth","config","credentials"}, + uri="resource://config", mime_type="application/json", fn=lambda: config ) try: + print("Adding configuration resource to MCP server...") mcp.add_resource(config_resource) except Exception as e: print(f"Warning: Failed to add config resource: {e}") From 555ece595de36670c9f8108632941342006e598e Mon Sep 17 00:00:00 2001 From: ckoegel Date: Tue, 12 Aug 2025 16:32:51 -0400 Subject: [PATCH 06/33] update gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 34d1312..f8de3fe 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,9 @@ coverage.xml .pytest_cache/ cover/ +# uv +uv.lock + # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: From 3907b8bfddd21cbe2145d52fd0be0890b807d342 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Tue, 12 Aug 2025 16:33:11 -0400 Subject: [PATCH 07/33] add pytest --- dev-requirements.txt | 2 ++ pyproject.toml | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 dev-requirements.txt diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..df48344 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,2 @@ +pytest>=8.4.1 +pytest-asyncio>=1.1.0 diff --git a/pyproject.toml b/pyproject.toml index c4131b8..da689d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,8 +10,14 @@ requires-python = ">=3.10" dependencies = [ "httpx~=0.28.0", "fastmcp~=2.11.0", - "pyyaml~=6.0.0" + "pyyaml~=6.0.0", ] [project.scripts] start = "app:main" + +[dependency-groups] +dev = [ + "pytest>=8.4.1", + "pytest-asyncio>=1.1.0", +] From 53f452a6f75ca4cd6efbf50adb8364f3e4137e58 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Tue, 12 Aug 2025 16:33:32 -0400 Subject: [PATCH 08/33] changes for tests --- src/__init__.py | 0 src/app.py | 5 +++-- src/server_utils.py | 3 ++- src/servers.py | 5 +++-- test/__init__.py | 0 5 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 src/__init__.py create mode 100644 test/__init__.py diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app.py b/src/app.py index fc68c6d..fdcf8aa 100644 --- a/src/app.py +++ b/src/app.py @@ -1,8 +1,9 @@ import asyncio from fastmcp import FastMCP -from servers import create_bandwidth_mcp -from config import ( + +from .servers import create_bandwidth_mcp +from .config import ( load_config, get_enabled_tools, get_excluded_tools diff --git a/src/server_utils.py b/src/server_utils.py index 250775b..ccb090c 100644 --- a/src/server_utils.py +++ b/src/server_utils.py @@ -4,11 +4,12 @@ import base64 from fastmcp import FastMCP -from resources import get_bandwidth_resources from fastmcp.resources import FunctionResource from fastmcp.server.openapi import MCPType, HTTPRoute from typing import Dict, List, Optional, Any, Callable +from .resources import get_bandwidth_resources + async def print_server_info(mcp: FastMCP) -> None: """Print concise server information.""" diff --git a/src/servers.py b/src/servers.py index 05d4760..749a26a 100644 --- a/src/servers.py +++ b/src/servers.py @@ -1,7 +1,8 @@ from fastmcp import FastMCP from httpx import AsyncClient from typing import Dict, List, Optional, Callable, Any -from server_utils import ( + +from .server_utils import ( add_resources, create_route_map_fn, create_auth_header, @@ -74,7 +75,7 @@ async def create_bandwidth_mcp( config: Configuration dictionary containing API credentials and other variables. Returns: - The FastMCP instance with all API servers imported + The FastMCP instance with all API servers imported and resources added. Raises: RuntimeError: If any API server fails to create or import diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 From 70172f3042fce75dab49b0cfd05eacb311ada870 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Tue, 12 Aug 2025 16:33:38 -0400 Subject: [PATCH 09/33] servers unit tests --- test/test_servers.py | 106 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 test/test_servers.py diff --git a/test/test_servers.py b/test/test_servers.py new file mode 100644 index 0000000..f86af8a --- /dev/null +++ b/test/test_servers.py @@ -0,0 +1,106 @@ +import pytest +from fastmcp import FastMCP +from src.servers import create_bandwidth_mcp, _create_server + +async def create_mcp_server(name=None, tools=None, excluded_tools=None): + """Fixture to create and return a FastMCP instance.""" + mcp = FastMCP(name=name or "Test MCP") + config = {"BW_USERNAME": "test_user", "BW_PASSWORD": "test_pass"} + enabled_tools = tools if tools is not None else [] + excluded_tools = excluded_tools if excluded_tools is not None else [] + + await create_bandwidth_mcp(mcp, enabled_tools, excluded_tools, config) + + return mcp + + +def calculate_expected_tools(tools, excluded_tools, total_tools=19): + if tools and not excluded_tools: + return len(tools) + elif excluded_tools: + return total_tools - len(excluded_tools) + else: + return total_tools + + +server_configuration_list = [ + ([], []), + ([], ["getReports", "createReport"]), + (["createMessage"], []), + (["generateMessagingCode", "generateVoiceCode"], []), + (["createLookup", "getLookupStatus", "createMessage"], ["listCalls"]), + (["listMedia"], ["uploadMedia", "deleteMedia", "getMedia"]), +] + +@pytest.mark.skip +@pytest.mark.asyncio +@pytest.mark.parametrize("tools, excluded_tools", server_configuration_list) +async def test_full_mcp_server_creation(tools, excluded_tools): + """Test that the MCP server is created correctly with included and excluded tools.""" + + expected_tools = calculate_expected_tools(tools, excluded_tools) + name = f"Test MCP with {expected_tools} Tools" + + mcp = await create_mcp_server(name, tools, excluded_tools) + mcp_tools = await mcp.get_tools() + mcp_tool_names = list(mcp_tools.keys()) + mcp_resources = await mcp.get_resources() + + assert isinstance(mcp, FastMCP) + assert mcp.name == name, f"Expected MCP name '{name}', got '{mcp.name}'" + assert len(mcp_tools) == expected_tools, f"Expected {expected_tools} tools, got {len(mcp_tools)}" + assert len(mcp_resources) == 2, f"Expected 2 resources, got {len(mcp_resources)}" + + if excluded_tools: + for tool in excluded_tools: + assert tool not in mcp_tool_names, f"Excluded tool {tool} should not be present" + + if tools and not excluded_tools: + for tool in tools: + assert tool in mcp_tool_names, f"Enabled tool {tool} should be present" + + +spec_list = [ + ( + "https://dev.bandwidth.com/spec/multi-factor-auth.yml", + {"BW_USERNAME": "test_user_mfa", "BW_PASSWORD": "test_pass_mfa"}, + "https://mfa.bandwidth.com/api/v1/", + {"generateMessagingCode", "generateVoiceCode", "verifyCode"}, + "Basic dGVzdF91c2VyX21mYTp0ZXN0X3Bhc3NfbWZh" + ), + ( + "https://dev.bandwidth.com/spec/phone-number-lookup.yml", + {"BW_USERNAME": "test_user_tnlookup", "BW_PASSWORD": "test_pass_tnlookup"}, + "https://numbers.bandwidth.com/api/v1/", + {"createLookup", "getLookupStatus"}, + "Basic dGVzdF91c2VyX3RubG9va3VwOnRlc3RfcGFzc190bmxvb2t1cA==" + ) +] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("url, config, expected_base_url, expected_tools, expected_auth_header", spec_list) +async def test_individual_mcp_server_creation( + url, + config, + expected_base_url, + expected_tools, + expected_auth_header +): + """Test that individual MCP servers are created correctly.""" + + server = await _create_server(url, None, config) + server_client = server._client + + server_tools = await server.get_tools() + server_tool_names = set(server_tools.keys()) + + assert isinstance(server, FastMCP) + assert server.name == "Bandwidth", f"Expected server name to be 'Bandwidth', got '{server.name}'" + assert server_tool_names == expected_tools, f"Expected tools {expected_tools}, got {server_tool_names}" + assert server_client.headers["User-Agent"] == "Bandwidth MCP Server", \ + f"Expected User-Agent 'Bandwidth MCP Server', got '{server_client.headers['User-Agent']}'" + assert server_client.base_url == expected_base_url, \ + f"Expected base URL '{expected_base_url}', got '{server_client.base_url}'" + assert server_client.headers["Authorization"] == expected_auth_header, \ + f"Expected auth header '{expected_auth_header}', got '{server_client.headers['Authorization']}'" From c4ea5caa373ceb46709ecdf7ac8e6bfea1ef9ab0 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Tue, 12 Aug 2025 16:35:59 -0400 Subject: [PATCH 10/33] add test workflow --- .github/workflows/test-pr.yml | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/test-pr.yml diff --git a/.github/workflows/test-pr.yml b/.github/workflows/test-pr.yml new file mode 100644 index 0000000..f1b1a6a --- /dev/null +++ b/.github/workflows/test-pr.yml @@ -0,0 +1,38 @@ +name: Test PR + +on: + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true + +jobs: + test: + name: Test PR + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-2025,ubuntu-24.04] + python-version: ['3.10', '3.11', '3.12', '3.13'] + fail-fast: false + env: + PYTHON_VERSION: ${{ matrix.python-version }} + OPERATING_SYSTEM: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Packages and Test + run: | + pip install -r requirements.txt + pip install -r dev-requirements.txt + pytest -v From 8224798c4ba7036b180901b9d8e6be4f209bc95b Mon Sep 17 00:00:00 2001 From: ckoegel Date: Tue, 12 Aug 2025 16:36:40 -0400 Subject: [PATCH 11/33] pytest-asyncio not necessary for now --- dev-requirements.txt | 1 - pyproject.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index df48344..3809092 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,2 +1 @@ pytest>=8.4.1 -pytest-asyncio>=1.1.0 diff --git a/pyproject.toml b/pyproject.toml index da689d1..27a3a6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,5 +19,4 @@ start = "app:main" [dependency-groups] dev = [ "pytest>=8.4.1", - "pytest-asyncio>=1.1.0", ] From cb8047e14f35ffd910a6f249f882e304303cd61d Mon Sep 17 00:00:00 2001 From: ckoegel Date: Wed, 20 Aug 2025 16:55:11 -0400 Subject: [PATCH 12/33] remove some unnecessary error handling --- src/server_utils.py | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/server_utils.py b/src/server_utils.py index ccb090c..cde276c 100644 --- a/src/server_utils.py +++ b/src/server_utils.py @@ -13,20 +13,16 @@ async def print_server_info(mcp: FastMCP) -> None: """Print concise server information.""" - try: - all_tools = await mcp.get_tools() - all_resources = await mcp.get_resources() - - tool_names = list(all_tools.keys()) - resource_names = [resource.name for resource in all_resources.values()] + + all_tools = await mcp.get_tools() + all_resources = await mcp.get_resources() + + tool_names = list(all_tools.keys()) + resource_names = [resource.name for resource in all_resources.values()] - print("Bandwidth MCP Server Started") - print(f"Tools ({len(tool_names)}): {', '.join(sorted(tool_names)) if tool_names else 'None'}") - print(f"Resources ({len(resource_names)}): {', '.join(sorted(resource_names)) if resource_names else 'None'}") - - except Exception as e: - print(f"Error retrieving server info: {e}") - print("Server may still be functional") + print("Bandwidth MCP Server Started") + print(f"Tools ({len(tool_names)}): {', '.join(sorted(tool_names)) if tool_names else 'None'}") + print(f"Resources ({len(resource_names)}): {', '.join(sorted(resource_names)) if resource_names else 'None'}") def create_route_map_fn( @@ -127,11 +123,7 @@ def add_resources(mcp: FastMCP, config: Dict[str, Any]) -> FastMCP: fn=lambda: config ) - try: - print("Adding configuration resource to MCP server...") - mcp.add_resource(config_resource) - except Exception as e: - print(f"Warning: Failed to add config resource: {e}") + mcp.add_resource(config_resource) for resource in get_bandwidth_resources(): try: From 0da14cd5f399464ab2991e71435bcfb2590bdc11 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Wed, 20 Aug 2025 16:55:43 -0400 Subject: [PATCH 13/33] add pytest-httpx --- dev-requirements.txt | 1 + pyproject.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index 3809092..bc7860e 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1 +1,2 @@ pytest>=8.4.1 +pytest-httpx>=0.35.0 diff --git a/pyproject.toml b/pyproject.toml index 27a3a6a..4d0a852 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,4 +19,5 @@ start = "app:main" [dependency-groups] dev = [ "pytest>=8.4.1", + "pytest-httpx>=0.35.0", ] From d49b50742b5c7f70c5b1ee2aba6df5cf25d50aee Mon Sep 17 00:00:00 2001 From: ckoegel Date: Wed, 20 Aug 2025 16:56:09 -0400 Subject: [PATCH 14/33] use mocking and update unit tests --- test/fixtures/empty.yml | 1 + test/fixtures/insights.yml | 1244 ++++++++++++++ test/fixtures/messaging.yml | 2267 +++++++++++++++++++++++++ test/fixtures/multi-factor-auth.yml | 311 ++++ test/fixtures/no-servers.yml | 57 + test/fixtures/phone-number-lookup.yml | 424 +++++ test/test_openapi.py | 40 + test/test_servers.py | 24 +- test/utils.py | 11 + 9 files changed, 4374 insertions(+), 5 deletions(-) create mode 100644 test/fixtures/empty.yml create mode 100644 test/fixtures/insights.yml create mode 100644 test/fixtures/messaging.yml create mode 100644 test/fixtures/multi-factor-auth.yml create mode 100644 test/fixtures/no-servers.yml create mode 100644 test/fixtures/phone-number-lookup.yml create mode 100644 test/test_openapi.py create mode 100644 test/utils.py diff --git a/test/fixtures/empty.yml b/test/fixtures/empty.yml new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/test/fixtures/empty.yml @@ -0,0 +1 @@ + diff --git a/test/fixtures/insights.yml b/test/fixtures/insights.yml new file mode 100644 index 0000000..6e73731 --- /dev/null +++ b/test/fixtures/insights.yml @@ -0,0 +1,1244 @@ +openapi: 3.0.1 +info: + title: Insights + contact: + name: Bandwidth + version: 0.1.2 + termsOfService: https://www.bandwidth.com/legal/terms-of-use-bandwidthcom-web-sites/ + description: |- + The API specification for Bandwidth's Insights platform. + + ## Base URL + `https://insights.bandwidth.com/api` +servers: + - url: https://insights.bandwidth.com/api +tags: + - name: Voice + description: Call Logs + - name: Reporting + description: Reporting Framework +paths: + /v1/voice/calls: + get: + tags: + - Voice + summary: List Calls + description: Returns a list of call events. + operationId: listCalls + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/callId" + - $ref: "#/components/parameters/startTime" + - $ref: "#/components/parameters/endTime" + - $ref: "#/components/parameters/callingNumber" + - $ref: "#/components/parameters/calledNumber" + - $ref: "#/components/parameters/callDirection" + - $ref: "#/components/parameters/callType" + - $ref: "#/components/parameters/callResult" + - $ref: "#/components/parameters/hangUpSource" + - $ref: "#/components/parameters/sipResponseCode" + - $ref: "#/components/parameters/subAccount" + - $ref: "#/components/parameters/locationId" + - $ref: "#/components/parameters/sourceCountryCodeA3" + - $ref: "#/components/parameters/destinationCountryCodeA3" + - $ref: "#/components/parameters/sourceIp" + - $ref: "#/components/parameters/destinationIp" + - $ref: "#/components/parameters/qualityStatus" + - $ref: "#/components/parameters/sort" + - $ref: "#/components/parameters/offset" + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/region" + responses: + "200": + $ref: "#/components/responses/listCallsResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotFoundError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /v1/voice/calls/{callId}: + get: + tags: + - Voice + summary: Get Call + description: Returns a single call event. + operationId: listCall + parameters: + - $ref: "#/components/parameters/callIdPath" + - $ref: "#/components/parameters/region" + responses: + "200": + $ref: "#/components/responses/getCallResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotFoundError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /v1/reports: + post: + tags: + - Reporting + description: Create a new report instance. + operationId: createReport + summary: Create Report + requestBody: + $ref: "#/components/requestBodies/createReportBody" + responses: + "200": + $ref: "#/components/responses/reportStatusResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotFoundError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + get: + tags: + - Reporting + description: Get a history of created reports. + operationId: getReports + summary: Get Reports + parameters: + - $ref: "#/components/parameters/accountIds" + - $ref: "#/components/parameters/categoryRequired" + responses: + "200": + $ref: "#/components/responses/reportHistoryResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotFoundError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /v1/reports/{reportId}: + get: + tags: + - Reporting + description: Get the status of a report. + operationId: getReportStatus + summary: Get Report Status + parameters: + - $ref: "#/components/parameters/reportIdPath" + responses: + "200": + $ref: "#/components/responses/reportStatusResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotFoundError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /v1/reports/{reportId}/file: + get: + tags: + - Reporting + description: Get the file associated with a report. + operationId: getReportFile + summary: Get Report File + parameters: + - $ref: "#/components/parameters/reportIdPath" + responses: + "200": + $ref: "#/components/responses/reportFileResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotFoundError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /v1/reportDefinitions: + get: + tags: + - Reporting + description: Get a list of report definitions. + operationId: getReportDefinitions + summary: Get Report Definitions + parameters: + - $ref: "#/components/parameters/category" + - $ref: "#/components/parameters/domain" + - $ref: "#/components/parameters/reportName" + responses: + "200": + $ref: "#/components/responses/reportDefinitionsResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotFoundError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" +components: + schemas: + reportStatusObject: + title: Create Report Request + type: object + properties: + category: + $ref: "#/components/schemas/category" + domain: + $ref: "#/components/schemas/domain" + reportName: + type: string + example: MDRs + accountIds: + type: array + items: + type: string + example: "123456" + example: [ "123456", "654321" ] + filters: + $ref: "#/components/schemas/filters" + region: + $ref: "#/components/schemas/region" + breakdown: + type: string + example: campaignId + description: The filter name to breakdown the report by. + reportStatus: + type: object + properties: + reportId: + type: string + example: 67a95525-4618-418b-ad8b-4ddd8562ad37 + format: uuid + requestedAt: + type: string + format: date-time + example: 2024-01-18T17:38:37.041444Z + completedAt: + type: string + format: date-time + example: 2024-01-18T17:38:37.041444Z + user: + type: string + example: Username + report: + $ref: "#/components/schemas/reportStatusObject" + status: + $ref: "#/components/schemas/status" + fileName: + type: string + example: report123.zip + reportDefinitionObject: + type: object + properties: + category: + $ref: "#/components/schemas/category" + domain: + $ref: "#/components/schemas/domain" + reportName: + type: string + example: MDRs + description: + type: string + example: Messaging details for particular snapshot date + regions: + type: array + items: + $ref: "#/components/schemas/region" + example: [ "US", "EU" ] + maxMonthsBack: + type: integer + example: 12 + minimum: 1 + maximum: 24 + filters: + type: array + items: + $ref: "#/components/schemas/rdoFilter" + breakdowns: + type: array + items: + type: object + properties: + value: + type: string + example: column_name + category: + type: string + enum: + - CHARGES + - NUMBER_DETAILS + - USAGE + example: USAGE + dropdownValues: + type: object + properties: + values: + type: array + items: + type: object + properties: + key: + type: string + example: column_name + domain: + type: string + enum: + - EMERGENCY + - MESSAGING + - VOICE + example: MESSAGING + operator: + type: string + enum: + - EQUAL + - GREATER + - LESSER + - LESSER_OR_EQUAL + - GREATER_OR_EQUAL + - LIKE + - IN + - STARTS_WITH + - BETWEEN + example: EQUAL + status: + type: string + enum: + - COMPLETED + - FAILED + - NO_RESULTS + - PROCESSING + example: COMPLETED + validationType: + type: string + enum: + - E164 + - REGEX + - ISO8601 + - ISO8601_RANGE + - STRING + - INTEGER + - ACCOUNT + example: E164 + filters: + description: Use the Get Report Definitions endpoint to see the filters available for a report. + type: object + additionalProperties: + type: array + items: + type: string + rdoFilter: + type: object + properties: + filterName: + type: string + example: My Filter + validationType: + $ref: "#/components/schemas/validationType" + dropdownValues: + type: array + items: + $ref: "#/components/schemas/dropdownValues" + operator: + $ref: "#/components/schemas/operator" + validationRegex: + type: string + example: '^\\d{10}$' + required: + type: boolean + example: true + call: + title: Call + type: object + properties: + accountId: + type: string + description: CRM ID of the account to get calls from. + example: "1234567" + callId: + type: string + description: Unique call identifier. + example: 1234567890abcdefghijklmnopqrstuvwxyz + startTime: + type: string + description: |- + Timestamp of when the call started, conforming to RFC 3339 format. + example: "2022-05-26T17:05:48.960Z" + endTime: + type: string + description: |- + Timestamp of when the call ended, conforming to RFC 3339 format. + example: "2022-05-26T17:27:44.400Z" + duration: + type: integer + nullable: true + description: Length of the call, measured in milliseconds. + example: 500000 + callingNumber: + type: string + nullable: true + description: |- + Phone number of the caller who initiated the call. + + Format: E.164 with '+' prefix + example: "+18185559876" + calledNumber: + type: string + nullable: true + description: |- + Phone number of the caller who received the call. + + Format: E.164 with '+' prefix + example: "+18185551234" + callDirection: + $ref: "#/components/schemas/callDirection" + callType: + $ref: "#/components/schemas/callType" + callResult: + $ref: "#/components/schemas/callResult" + sipResponseCode: + type: integer + description: Message generated by a user agent server (UAS) or SIP server to reply to a request generated by a client. A value of negative one (-1) indicates an unknown SIP response. + example: 200 + sipResponseDescription: + type: string + description: A short description of the SIP response. + example: "OK" + cost: + format: double + type: number + nullable: true + description: Estimated cost associated with the call. This may not match directly to your invoice as this is an estimate. If this value is null it is because the cost has not yet been calculated. Cost is usually available within a day of call completion. + example: 0.0001230 + subAccount: + type: string + nullable: true + description: Sub-Account ID. + example: "1234" + subAccountName: + type: string + nullable: true + description: Sub-Account Name. + example: "Account 1234" + locationId: + type: string + nullable: true + description: Location ID. + example: "1234" + locationName: + type: string + nullable: true + description: Location Name. + example: "Location 1234" + sourceCountryCodeA3: + type: string + nullable: true + description: Source Country in A3 format. + example: USA + destinationCountryCodeA3: + type: string + nullable: true + description: Destination Country in A3 format. + example: + USA + sourceIp: + type: string + nullable: true + description: Source ip in ipv4 or ipv6 format. + example: + 168.212.226.204 + destinationIp: + type: string + nullable: true + description: Destination ip in ipv4 or ipv6 format. + example: + 168.212.226.204 + customerJitter: + type: integer + nullable: true + description: Jitter experienced by the customer during the call, measured in milliseconds. + example: + 500 + customerLatency: + type: integer + nullable: true + description: Latency experienced by the customer during the call, measured in milliseconds. + example: + 500 + customerPacketLossPercentage: + type: number + nullable: true + description: Packet loss percentage experienced by the customer during the call. + example: + 50.05 + carrierJitter: + type: integer + nullable: true + description: Jitter experienced by the carrrier during the call, measured in milliseconds. + example: + 500 + carrierLatency: + type: integer + nullable: true + description: Latency experienced by the carrier during the call, measured in milliseconds. + example: + 500 + carrierPacketLossPercentage: + type: number + nullable: true + description: Packet loss percentage experienced by the carrier during the call. + example: + 50.05 + attestationIndicator: + $ref: "#/components/schemas/attestationIndicator" + x5u: + type: string + nullable: true + description: X5U URL of the certificate’s location. + example: "https://example.com/x5u" + hangUpSource: + $ref: "#/components/schemas/hangUpSource" + postDialDelay: + type: integer + description: How long it takes for a calling party to hear a ringback tone after initiating a call, measured in milliseconds. + nullable: true + example: 0 + packetsSent: + type: integer + description: Count of packets sent during transmission. + nullable: true + example: 0 + packetsReceived: + type: integer + description: Count of packets received during transmission. + nullable: true + example: 0 + genericError: + title: Generic Error + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + nullable: true + example: null + data: + type: object + nullable: true + example: null + errors: + type: array + items: + $ref: "#/components/schemas/error" + nullable: true + required: + - links + - data + - errors + callDirection: + type: string + description: Direction of call. + enum: + - INBOUND + - INBOUND-FORWARDED + - OUTBOUND + - OUTBOUND-FORWARDED + example: INBOUND + callType: + type: string + description: Type of call. + enum: + - EMERGENCY + - INBOUND-TFOOS + - INFORMATION + - INTERNATIONAL + - INTERNATIONAL-INTERNAL + - INTERSTATE + - INTRASTATE + - INTL-BLOCK + - LOCAL + - OPERATOR + - OTHER-N11 + - SIPURI-EXT + - TOLLFREE-IN + - TOLLFREE-OUT + - UNDETERMINED + example: LOCAL + callResult: + type: string + description: Call completion status. + enum: + - COMPLETE + - INCOMPLETE + example: COMPLETE + attestationIndicator: + type: string + description: STIR/SHAKEN attestation. + nullable: true + enum: + - A + - B + - C + example: A + hangUpSource: + type: string + description: Hang up source. + enum: + - BANDWIDTH_INTERNAL + - CALLED_PARTY + - CALLING_PARTY + example: CALLED_PARTY + countryCodeA3: + type: string + description: |- + Filter Type: Exact Match, Multimatch. + + Example: + + * Exact Match: USA + + * Multimatch: USA,IRL + + Format: two-letter (A3) country code . + ipAddress: + type: string + description: |- + Filter Type: Exact Match. + + Example: + + * Exact Match: 168.212.226.204 + * Exact Match: 2001%3Adb8%3A3333%3A4444%3A5555%3A6666%3A7777%3A8888 + + Format: ipv4, ipv6 format ( : is url encoded as %3A) + example: 168.212.226.204 + qualityStatus: + type: string + description: A combination of Jitter, Latency, and Packet Loss Percentage to determine the overall quality of the call. Quality Status filter is only available for accounts with Advanced Quality Metrics feature. + enum: + - GOOD + - AVERAGE + - BAD + example: GOOD + region: + type: string + enum: + - US + - EU + example: US + default: US + link: + title: Link + type: object + properties: + href: + type: string + description: URI of the link. + example: self + rel: + type: string + description: Specifies the relationship between this link and the resource. + example: https://insights.bandwidth.com/api/v1/resource + error: + title: Error + type: object + properties: + description: + type: string + description: A human-readable explanation that SHOULD be specific to this occurrence of the problem. + example: There was an issue with a field in your request body. + required: + - description + parameters: + accountIds: + name: accountIds + in: query + schema: + type: array + items: + type: string + categoryRequired: + name: category + in: query + required: true + schema: + type: string + enum: + - CHARGES + - NUMBER_DETAILS + - USAGE + category: + name: category + in: query + schema: + type: string + enum: + - CHARGES + - NUMBER_DETAILS + - USAGE + domain: + name: domain + in: query + schema: + type: string + enum: + - EMERGENCY + - MESSAGING + - VOICE + reportIdPath: + name: reportId + in: path + description: The ID of the report. + required: true + schema: + type: string + reportName: + name: reportName + in: query + schema: + type: string + accountId: + name: accountId + in: query + description: CRM ID of the account to get calls from. + schema: + type: string + example: "1234567" + callId: + name: callId + in: query + description: | + Exact Match, any valid call ID. + schema: + type: string + example: 12345678980abcdefghijklmnopqrstuvwxyz + callIdPath: + name: callId + in: path + required: true + description: |- + Filter Type: Exact Match, any valid call ID. + schema: + type: string + example: 12345678980abcdefghijklmnopqrstuvwxyz + startTime: + name: startTime + in: query + description: | + Filter Type: Range using gt, gte, lt, and lte. + + Note: If no startTime or endTime is specified, startTime will default to the last 24 hours. + schema: + type: string + example: | + gte:2022-05-26T12:00:00Z + endTime: + name: endTime + in: query + description: | + Filter Type: Range using gt, gte, lt, and lte. + schema: + type: string + example: |- + lte:2022-05-26T12:00:50Z + callingNumber: + name: callingNumber + in: query + description: |- + Filter Type: Exact Match, Multimatch, Prefix Match. + + Example: + + * Exact Match: 15555551234 + + * Multimatch: 15555551234,15555554321 + + * Prefix: 155555512* + + Format: E.164 with an optional leading '+' (URL encoded as %2B) + schema: + type: string + example: "%2B18185559876" + calledNumber: + name: calledNumber + in: query + description: |- + Filter Type: Exact Match, Multimatch, Prefix Match. + + Example: + + * Exact Match: 15555551234 + + * Multimatch: 15555551234,15555554321 + + * Prefix: 155555512* + + Format: E.164 with an optional leading '+' (URL encoded as %2B) + schema: + type: string + example: "%2B18185551234" + callDirection: + name: callDirection + in: query + description: |- + Filter Type: Exact Match, any valid call direction type. + schema: + $ref: "#/components/schemas/callDirection" + callType: + name: callType + in: query + description: |- + Filter Type: Exact Match, any valid call type. + schema: + $ref: "#/components/schemas/callType" + callResult: + name: callResult + in: query + description: |- + Filter Type: Exact Match, any valid call result type. + schema: + $ref: "#/components/schemas/callResult" + hangUpSource: + name: hangUpSource + in: query + description: |- + Filter Type: Exact Match, any valid hang up source. + schema: + $ref: "#/components/schemas/hangUpSource" + sipResponseCode: + name: sipResponseCode + in: query + description: |- + Filter Type: Exact Match. Examples: 200, 300, 400, 500. + schema: + type: integer + example: 200 + subAccount: + name: subAccount + in: query + description: |- + Filter Type: Exact Match. Examples: 1234,2345 + schema: + type: string + example: "1234" + locationId: + name: locationId + in: query + description: |- + Filter Type: Exact Match. Examples: 1234,2345 + schema: + type: string + example: "1234" + sourceCountryCodeA3: + name: sourceCountryCodeA3 + in: query + schema: + $ref: "#/components/schemas/countryCodeA3" + destinationCountryCodeA3: + name: destinationCountryCodeA3 + in: query + schema: + $ref: "#/components/schemas/countryCodeA3" + sourceIp: + name: sourceIp + in: query + schema: + $ref: "#/components/schemas/ipAddress" + destinationIp: + name: destinationIp + in: query + schema: + $ref: "#/components/schemas/ipAddress" + qualityStatus: + name: qualityStatus + in: query + schema: + $ref: "#/components/schemas/qualityStatus" + sort: + name: sort + in: query + description: |- + The field and direction to sort by, combined with a colon. Direction is one of asc, desc. + schema: + type: string + example: startTime:desc + offset: + name: offset + in: query + description: |- + Return records starting at the nth record. + schema: + type: integer + example: 1 + limit: + name: limit + in: query + description: |- + The maximum records to return per page. + schema: + type: integer + example: 50 + default: 100 + minimum: 1 + maximum: 10000 + region: + name: region + in: query + description: |- + Filter Type: Exact Match, any valid region. + schema: + $ref: "#/components/schemas/region" + requestBodies: + createReportBody: + content: + application/json: + schema: + $ref: "#/components/schemas/reportStatusObject" + responses: + reportStatusResponse: + description: OK + content: + application/json: + schema: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + example: + - rel: "reportDownload" + href: "https://insights.bandwidth.com/api/v1/reports/67a95525-4618-418b-ad8b-4ddd8562ad37/file" + data: + $ref: "#/components/schemas/reportStatus" + errors: + type: array + items: + $ref: "#/components/schemas/error" + example: [ ] + required: + - links + - data + - errors + reportFileResponse: + description: OK + content: + application/gzip: + schema: + type: string + format: binary + headers: + Content-Disposition: + schema: + type: string + example: attachment; filename="report123.zip" + Content-Length: + schema: + type: integer + example: 12345 + reportHistoryResponse: + description: OK + content: + application/json: + schema: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + example: [ ] + data: + type: object + properties: + totalCount: + type: integer + example: 1 + reports: + type: array + items: + $ref: "#/components/schemas/reportStatus" + errors: + type: array + items: + $ref: "#/components/schemas/error" + example: [ ] + required: + - links + - data + - errors + reportDefinitionsResponse: + description: OK + content: + application/json: + schema: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + example: [ ] + data: + type: object + properties: + reportDefinitions: + type: array + items: + $ref: "#/components/schemas/reportDefinitionObject" + errors: + type: array + items: + $ref: "#/components/schemas/error" + example: [ ] + required: + - links + - data + - errors + listCallsResponse: + description: OK + content: + application/json: + schema: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + example: + - rel: "next" + href: "https://insights.bandwidth.com/api/v1/voice/calls?callResult=completed&offset=1&limit=1&sort=startTime:desc" + nullable: true + data: + type: object + properties: + totalCount: + type: integer + description: The total count of objects returned in the response. + example: 1 + calls: + type: array + items: + $ref: "#/components/schemas/call" + nullable: true + errors: + type: array + items: + $ref: "#/components/schemas/error" + nullable: true + example: null + required: + - links + - data + - errors + getCallResponse: + description: OK + content: + application/json: + schema: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + nullable: false + example: [] + data: + $ref: "#/components/schemas/call" + errors: + type: array + items: + $ref: "#/components/schemas/error" + nullable: false + example: [] + required: + - links + - data + - errors + badRequestError: + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/genericError" + examples: + badRequestErrorExample: + $ref: "#/components/examples/badRequestErrorExample" + unauthorizedError: + description: Unauthorized Account + content: + application/json: + schema: + $ref: "#/components/schemas/genericError" + examples: + unknownAccountErrorExample: + $ref: "#/components/examples/unauthorizedErrorExample" + forbiddenError: + description: Unknown Account + content: + application/json: + schema: + $ref: "#/components/schemas/genericError" + examples: + unknownAccountErrorExample: + $ref: "#/components/examples/forbiddenErrorExample" + notFoundError: + description: Method Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/genericError" + examples: + unknownAccountErrorExample: + $ref: "#/components/examples/notFoundErrorExample" + methodNotFoundError: + description: Method Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/genericError" + examples: + unknownAccountErrorExample: + $ref: "#/components/examples/methodNotFoundErrorExample" + tooManyRequestsError: + description: Too Many Requests Error + content: + application/json: + schema: + $ref: "#/components/schemas/genericError" + examples: + unknownAccountErrorExample: + $ref: "#/components/examples/tooManyRequestsErrorExample" + internalServerError: + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/genericError" + examples: + unknownAccountErrorExample: + $ref: "#/components/examples/internalServerErrorExample" + examples: + badRequestErrorExample: + summary: An example of a generic Bad Request Error + value: + links: null + data: null + errors: + - description: "There was an issue with your request." + unauthorizedErrorExample: + summary: An example of a generic Unauthorized Account Error + value: + links: null + data: null + errors: + - description: "This is an Unauthorized Account." + forbiddenErrorExample: + summary: An example of a generic Unknown Account Error + value: + links: null + data: null + errors: + - description: "This is an Unknown Account." + notFoundErrorExample: + summary: An example of a generic Not Found Error + value: + links: null + data: null + errors: + - description: "The origin server did not find a current representation for the target resource or is not willing to disclose that one exists." + methodNotFoundErrorExample: + summary: An example of a generic Method Not Found Error + value: + links: null + data: null + errors: + - description: "POST is not an allowed method. Method must be GET" + tooManyRequestsErrorExample: + summary: An example of a generic Too Many Requests Error + value: + links: null + data: null + errors: + - description: "The user has sent too many requests in a given amount of time." + internalServerErrorExample: + summary: An example of a generic Internal Server Error + value: + links: null + data: null + errors: + - description: "The server encountered an unexpected condition that prevented it from fulfilling the request." + securitySchemes: + Basic: + type: http + scheme: basic + description: |- + Basic authentication is a simple authentication scheme built into the HTTP protocol. To use it, send your HTTP requests with an `Authorization` header that contains the word `Basic` followed by a space and a Base64-encoded string `username:password`. + + - Example: `Authorization: Basic ZGVtbZpwQDU1dzByZA==` + Bearer: + type: http + scheme: bearer + bearerFormat: JWT + description: |- + Bearer authentication (also called token authentication) is an HTTP authentication scheme that involves security tokens called bearer tokens. The name “Bearer authentication” can be understood as “give access to the bearer of this token.” The client must send this token in the `Authorization` header when making requests to protected resources. + + - Example: `Authorization: Bearer ` + + Where `` should be replaced with the token string without angle brackets. + + For more information, see the following guide: [Credentials](https://dev.bandwidth.com/docs/credentials) +security: + - Basic: [] + - Bearer: [] diff --git a/test/fixtures/messaging.yml b/test/fixtures/messaging.yml new file mode 100644 index 0000000..d97db28 --- /dev/null +++ b/test/fixtures/messaging.yml @@ -0,0 +1,2267 @@ +openapi: 3.0.3 +info: + title: Messaging + version: 4.3.0 + contact: + name: Bandwidth + url: https://support.bandwidth.com + email: support@bandwidth.com + termsOfService: https://www.bandwidth.com/legal/terms-of-use-bandwidthcom-web-sites/ + description: |- + The API Specification for Bandwidth's Messaging Platform + + ## Base URL + + `https://messaging.bandwidth.com/api/v2` +servers: + - url: https://messaging.bandwidth.com/api/v2 + description: Production +paths: + /users/{accountId}/media: + get: + summary: List Media + description: |- + Gets a list of your media files. No query parameters are supported. + operationId: listMedia + tags: + - Media + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/continuationToken" + responses: + "200": + $ref: "#/components/responses/listMediaResponse" + "400": + $ref: "#/components/responses/messagingBadRequestError" + "401": + $ref: "#/components/responses/messagingUnauthorizedError" + "403": + $ref: "#/components/responses/messagingForbiddenError" + "404": + $ref: "#/components/responses/messagingNotFoundError" + "406": + $ref: '#/components/responses/messagingNotAcceptableError' + "415": + $ref: "#/components/responses/messagingInvalidMediaTypeError" + "429": + $ref: "#/components/responses/messagingTooManyRequestsError" + "500": + $ref: "#/components/responses/messagingInternalServerError" + /users/{accountId}/media/{mediaId}: + get: + summary: Get Media + description: Downloads a media file you previously uploaded. + operationId: getMedia + tags: + - Media + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/mediaId" + responses: + "200": + $ref: "#/components/responses/getMediaResponse" + "400": + $ref: "#/components/responses/messagingBadRequestError" + "401": + $ref: "#/components/responses/messagingUnauthorizedError" + "403": + $ref: "#/components/responses/messagingForbiddenError" + "404": + $ref: "#/components/responses/messagingNotFoundError" + "406": + $ref: '#/components/responses/messagingNotAcceptableError' + "415": + $ref: "#/components/responses/messagingInvalidMediaTypeError" + "429": + $ref: "#/components/responses/messagingTooManyRequestsError" + "500": + $ref: "#/components/responses/messagingInternalServerError" + put: + summary: Upload Media + description: |- + Upload a file. You may add headers to the request in order to provide some control to your media file. + + If a file is uploaded with the same name as a file that already exists under this account, the previous file will be overwritten. + + A list of supported media types can be found [here](https://support.bandwidth.com/hc/en-us/articles/360014128994-What-MMS-file-types-are-supported-). + operationId: uploadMedia + tags: + - Media + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/mediaId" + - $ref: "#/components/parameters/contentType" + - $ref: "#/components/parameters/cacheControl" + requestBody: + $ref: "#/components/requestBodies/uploadMediaRequest" + responses: + "204": + description: No Content + "400": + $ref: "#/components/responses/messagingBadRequestError" + "401": + $ref: "#/components/responses/messagingUnauthorizedError" + "403": + $ref: "#/components/responses/messagingForbiddenError" + "404": + $ref: "#/components/responses/messagingNotFoundError" + "406": + $ref: '#/components/responses/messagingNotAcceptableError' + "415": + $ref: "#/components/responses/messagingInvalidMediaTypeError" + "429": + $ref: "#/components/responses/messagingTooManyRequestsError" + "500": + $ref: "#/components/responses/messagingInternalServerError" + delete: + summary: Delete Media + description: |- + Deletes a media file from Bandwidth API server. Make sure you don't have + any application scripts still using the media before you delete. + + If you accidentally delete a media file you can immediately upload a new + file with the same name. + operationId: deleteMedia + tags: + - Media + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/mediaId" + responses: + "204": + description: No Content + "400": + $ref: "#/components/responses/messagingBadRequestError" + "401": + $ref: "#/components/responses/messagingUnauthorizedError" + "403": + $ref: "#/components/responses/messagingForbiddenError" + "404": + $ref: "#/components/responses/messagingNotFoundError" + "406": + $ref: '#/components/responses/messagingNotAcceptableError' + "415": + $ref: "#/components/responses/messagingInvalidMediaTypeError" + "429": + $ref: "#/components/responses/messagingTooManyRequestsError" + "500": + $ref: "#/components/responses/messagingInternalServerError" + /users/{accountId}/messages: + get: + summary: List Messages + description: Returns a list of messages based on query parameters. + operationId: listMessages + tags: + - Messages + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/messageId" + - $ref: "#/components/parameters/sourceTn" + - $ref: "#/components/parameters/destinationTn" + - $ref: "#/components/parameters/messageStatus" + - $ref: "#/components/parameters/messageDirection" + - $ref: "#/components/parameters/carrierName" + - $ref: "#/components/parameters/messageType" + - $ref: "#/components/parameters/errorCode" + - $ref: "#/components/parameters/fromDateTime" + - $ref: "#/components/parameters/toDateTime" + - $ref: "#/components/parameters/campaignId" + - $ref: "#/components/parameters/fromBwLatency" + - $ref: "#/components/parameters/bwQueued" + - $ref: "#/components/parameters/product" + - $ref: "#/components/parameters/location" + - $ref: "#/components/parameters/callingNumberCountryA3" + - $ref: "#/components/parameters/calledNumberCountryA3" + - $ref: "#/components/parameters/fromSegmentCount" + - $ref: "#/components/parameters/toSegmentCount" + - $ref: "#/components/parameters/fromMessageSize" + - $ref: "#/components/parameters/toMessageSize" + - $ref: "#/components/parameters/sort" + - $ref: "#/components/parameters/pageToken" + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/limitTotalCount" + responses: + "200": + $ref: "#/components/responses/listMessagesResponse" + "400": + $ref: "#/components/responses/messagingBadRequestError" + "401": + $ref: "#/components/responses/messagingUnauthorizedError" + "403": + $ref: "#/components/responses/messagingForbiddenError" + "404": + $ref: "#/components/responses/messagingNotFoundError" + "415": + $ref: "#/components/responses/messagingInvalidMediaTypeError" + "429": + $ref: "#/components/responses/messagingTooManyRequestsError" + "500": + $ref: "#/components/responses/messagingInternalServerError" + post: + summary: Create Message + description: Endpoint for sending text messages and picture messages using V2 messaging. + operationId: createMessage + tags: + - Messages + parameters: + - $ref: "#/components/parameters/accountId" + requestBody: + $ref: "#/components/requestBodies/createMessageRequest" + responses: + "202": + $ref: "#/components/responses/createMessageResponse" + "400": + $ref: "#/components/responses/createMessageBadRequestError" + "401": + $ref: "#/components/responses/messagingUnauthorizedError" + "403": + $ref: "#/components/responses/messagingForbiddenError" + "404": + $ref: "#/components/responses/messagingNotFoundError" + "405": + $ref: "#/components/responses/messagingMethodNotAllowedError" + "406": + $ref: '#/components/responses/messagingNotAcceptableError' + "415": + $ref: "#/components/responses/messagingInvalidMediaTypeError" + "429": + $ref: "#/components/responses/messagingTooManyRequestsError" + "500": + $ref: "#/components/responses/messagingInternalServerError" + callbacks: + statusCallback: + $ref: "#/components/callbacks/statusCallback" + /users/{accountId}/messages/multiChannel: + post: + summary: Create Multi-Channel Message + description: Endpoint for sending Multi-Channel messages. + operationId: createMultiChannelMessage + parameters: + - $ref: "#/components/parameters/accountId" + tags: + - Multi-Channel + requestBody: + $ref: "#/components/requestBodies/createMultiChannelMessageRequest" + responses: + "202": + $ref: "#/components/responses/createMultiChannelMessageResponse" + "400": + $ref: "#/components/responses/multiChannelBadRequestError" + "401": + $ref: "#/components/responses/multiChannelUnauthorizedError" + "403": + $ref: "#/components/responses/multiChannelForbiddenError" + "404": + $ref: "#/components/responses/multiChannelNotFoundError" + "405": + $ref: "#/components/responses/multiChannelMethodNotAllowedError" + "406": + $ref: '#/components/responses/multiChannelNotAcceptableError' + "415": + $ref: "#/components/responses/multiChannelInvalidMediaTypeError" + "429": + $ref: "#/components/responses/multiChannelTooManyRequestsError" + "500": + $ref: "#/components/responses/multiChannelInternalServerError" + callbacks: + statusCallback: + $ref: "#/components/callbacks/statusCallback" + x-badges: + - name: Beta + color: '#076EA8' +components: + parameters: + accountId: + in: path + name: accountId + required: true + schema: + type: string + description: Your Bandwidth Account ID. + example: "9900000" + mediaId: + in: path + name: mediaId + required: true + description: Media ID to retrieve. + example: 14762070468292kw2fuqty55yp2b2/0/bw.png + schema: + type: string + contentType: + in: header + name: Content-Type + style: simple + explode: false + description: The media type of the entity-body. + example: audio/wav + schema: + type: string + cacheControl: + in: header + name: Cache-Control + style: simple + explode: false + description: >- + General-header field is used to specify directives that MUST be obeyed by + all caching mechanisms along the request/response chain. + example: no-cache + schema: + type: string + continuationToken: + in: header + name: Continuation-Token + description: Continuation token used to retrieve subsequent media. + example: "1XEi2tsFtLo1JbtLwETnM1ZJ+PqAa8w6ENvC5QKvwyrCDYII663Gy5M4s40owR1tjkuWUif6qbWvFtQJR5/ipqbUnfAqL254LKNlPy6tATCzioKSuHuOqgzloDkSwRtX0LtcL2otHS69hK343m+SjdL+vlj71tT39" + schema: + type: string + messageId: + in: query + name: messageId + required: false + description: >- + The ID of the message to search for. Special characters need to be + encoded using URL encoding. Message IDs could come in different formats, + e.g., 9e0df4ca-b18d-40d7-a59f-82fcdf5ae8e6 and + 1589228074636lm4k2je7j7jklbn2 are valid message ID formats. Note that you + must include at least one query parameter. + example: 9e0df4ca-b18d-40d7-a59f-82fcdf5ae8e6 + schema: + type: string + sourceTn: + in: query + name: sourceTn + required: false + description: >- + The phone number that sent the message. Accepted values are: a single + full phone number a comma separated list of full phone numbers (maximum + of 10) or a single partial phone number (minimum of 5 characters e.g. + '%2B1919'). + example: "%2B15554443333" + schema: + type: string + destinationTn: + in: query + name: destinationTn + required: false + description: >- + The phone number that received the message. Accepted values are: a single + full phone number a comma separated list of full phone numbers (maximum + of 10) or a single partial phone number (minimum of 5 characters e.g. + '%2B1919'). + example: "%2B15554443333" + schema: + type: string + messageStatus: + in: query + name: messageStatus + required: false + description: >- + The status of the message. One of RECEIVED QUEUED SENDING SENT FAILED + DELIVERED ACCEPTED UNDELIVERED. + schema: + $ref: "#/components/schemas/messageStatusEnum" + messageDirection: + in: query + name: messageDirection + required: false + description: The direction of the message. One of INBOUND OUTBOUND. + schema: + $ref: "#/components/schemas/listMessageDirectionEnum" + carrierName: + in: query + name: carrierName + required: false + description: >- + The name of the carrier used for this message. Possible values include + but are not limited to Verizon and TMobile. Special characters need to + be encoded using URL encoding (i.e. AT&T should be passed as AT%26T). + example: Verizon + schema: + type: string + messageType: + in: query + name: messageType + required: false + description: The type of message. Either sms or mms. + schema: + $ref: "#/components/schemas/messageTypeEnum" + errorCode: + in: query + name: errorCode + required: false + description: The error code of the message. + example: 9902 + schema: + type: integer + fromDateTime: + in: query + name: fromDateTime + required: false + description: >- + The start of the date range to search in ISO 8601 format. Uses the + message receive time. The date range to search in is currently 14 days. + example: 2022-09-14T18:20:16.000Z + schema: + type: string + toDateTime: + in: query + name: toDateTime + required: false + description: >- + The end of the date range to search in ISO 8601 format. Uses the message + receive time. The date range to search in is currently 14 days. + example: 2022-09-14T18:20:16.000Z + schema: + type: string + campaignId: + in: query + name: campaignId + required: false + description: The campaign ID of the message. + example: CJEUMDK + schema: + type: string + fromBwLatency: + in: query + name: fromBwLatency + required: false + description: >- + The minimum Bandwidth latency of the message in seconds. Only available for accounts with the Advanced Quality Metrics feature enabled. + example: 5 + schema: + type: integer + bwQueued: + in: query + name: bwQueued + required: false + description: >- + A boolean value indicating whether the message is queued in the Bandwidth network. + example: true + schema: + type: boolean + product: + in: query + name: product + required: false + description: Messaging product associated with the message. + example: 'P2P' + schema: + $ref: "#/components/schemas/productTypeEnum" + location: + in: query + name: location + required: false + description: Location Id associated with the message. + example: '123ABC' + schema: + type: string + callingNumberCountryA3: + in: query + name: callingNumberCountryA3 + required: false + description: Calling number country in A3 format. + example: 'USA' + schema: + type: string + calledNumberCountryA3: + in: query + name: calledNumberCountryA3 + required: false + description: Called number country in A3 format. + example: 'USA' + schema: + type: string + fromSegmentCount: + in: query + name: fromSegmentCount + required: false + description: Segment count (start range). + example: 1 + schema: + type: integer + toSegmentCount: + in: query + name: toSegmentCount + required: false + description: Segment count (end range). + example: 3 + schema: + type: integer + fromMessageSize: + in: query + name: fromMessageSize + required: false + description: Message size (start range). + example: 100 + schema: + type: integer + toMessageSize: + in: query + name: toMessageSize + required: false + description: Message size (end range). + example: 120 + schema: + type: integer + sort: + in: query + name: sort + required: false + description: The field and direction to sort by combined with a colon. Direction is either asc or desc. + example: sourceTn:desc + schema: + type: string + pageToken: + in: query + name: pageToken + required: false + description: A base64 encoded value used for pagination of results. + example: gdEewhcJLQRB5 + schema: + type: string + limit: + in: query + name: limit + required: false + description: >- + The maximum records requested in search result. Default 100. The sum of + limit and after cannot be more than 10000. + schema: + type: integer + example: 50 + limitTotalCount: + in: query + name: limitTotalCount + required: false + description: When set to true, the response's totalCount field will have a maximum value of + 10,000. When set to false, or excluded, this will give an accurate totalCount of all + messages that match the provided filters. If you are experiencing latency, try using this + parameter to limit your results. + example: true + schema: + type: boolean + schemas: + applicationId: + type: string + description: The ID of the Application your from number or senderId is associated with in the Bandwidth Phone Number Dashboard. + example: 93de2206-9669-4e07-948d-329f4b722ee2 + priorityEnum: + type: string + description: >- + Specifies the message's sending priority with respect to other messages in your account. + For best results and optimal throughput, reserve the 'high' priority setting for critical messages only. + enum: + - default + - high + example: default + messageStatusEnum: + type: string + description: >- + The status of the message. One of RECEIVED QUEUED SENDING SENT FAILED + DELIVERED ACCEPTED UNDELIVERED. + enum: + - RECEIVED + - QUEUED + - SENDING + - SENT + - FAILED + - DELIVERED + - ACCEPTED + - UNDELIVERED + example: "RECEIVED" + listMessageDirectionEnum: + type: string + description: The direction of the message. One of INBOUND OUTBOUND. + enum: + - INBOUND + - OUTBOUND + example: INBOUND + messageDirectionEnum: + type: string + description: The direction of the message. One of in out. + enum: + - in + - out + example: in + messageTypeEnum: + type: string + description: The type of message. Either SMS or MMS. + enum: + - sms + - mms + - rcs + example: 'sms' + productTypeEnum: + type: string + description: The type of product associated with the message. + enum: + - LOCAL_A2P + - P2P + - SHORT_CODE_REACH + - TOLL_FREE + - HOSTED_SHORT_CODE + - ALPHA_NUMERIC + - RBM_MEDIA + - RBM_RICH + - RBM_CONVERSATIONAL + example: 'P2P' + fieldError: + type: object + properties: + fieldName: + type: string + description: The name of the field that contains the error + example: from + description: + type: string + description: The error associated with the field + example: "'+invalid' must be replaced with a valid E164 formatted telephone number" + messagesList: + title: MessagesList + type: object + properties: + totalCount: + type: integer + description: The total number of messages matched by the search. When the request has limitTotalCount set to true this value is limited to 10,000. + example: 100 + pageInfo: + $ref: "#/components/schemas/pageInfo" + messages: + type: array + items: + $ref: "#/components/schemas/listMessageItem" + listMessageItem: + title: listMessageItem + type: object + properties: + messageId: + type: string + description: The message id + example: 1589228074636lm4k2je7j7jklbn2 + accountId: + type: string + description: The account id associated with this message. + example: "9900000" + sourceTn: + type: string + description: The source phone number of the message. + example: "+15554443333" + destinationTn: + type: string + description: The recipient phone number of the message. + example: "+15554442222" + messageStatus: + $ref: "#/components/schemas/messageStatusEnum" + messageDirection: + $ref: "#/components/schemas/listMessageDirectionEnum" + messageType: + $ref: "#/components/schemas/messageTypeEnum" + segmentCount: + $ref: "#/components/schemas/segmentCount" + errorCode: + type: integer + description: The numeric error code of the message. + example: 9902 + receiveTime: + type: string + format: date-time + description: The ISO 8601 datetime of the message. + example: 2020-04-07T14:03:07.000Z + carrierName: + type: string + nullable: true + description: The name of the carrier. Not currently supported for MMS coming soon. + example: other + messageSize: + type: integer + description: The size of the message including message content and headers. + nullable: true + example: 27 + messageLength: + type: integer + description: The length of the message content. + example: 18 + attachmentCount: + type: integer + description: The number of attachments the message has. + nullable: true + example: 1 + recipientCount: + type: integer + description: The number of recipients the message has. + nullable: true + example: 1 + campaignClass: + type: string + description: The campaign class of the message if it has one. + nullable: true + example: T + campaignId: + type: string + description: The campaign ID of the message if it has one. + nullable: true + example: CJEUMDK + bwLatency: + type: integer + description: The Bandwidth latency of the message in seconds. Only available for accounts with the Advanced Quality Metrics feature enabled. + nullable: true + example: 20 + callingNumberCountryA3: + type: string + description: The A3 country code of the calling number. + nullable: true + example: 'USA' + calledNumberCountryA3: + type: string + description: The A3 country code of the called number. + nullable: true + example: 'USA' + product: + type: string + description: The messaging product associated with the message. + nullable: true + example: 'P2P' + location: + type: string + description: The location ID associated with this message. + nullable: true + example: '123ID' + pageInfo: + title: PageInfo + type: object + properties: + prevPage: + type: string + description: The link to the previous page for pagination. + example: https://messaging.bandwidth.com/api/v2/users/accountId/messages?messageStatus=DLR_EXPIRED&nextPage=DLAPE902 + nextPage: + type: string + description: The link to the next page for pagination. + example: https://messaging.bandwidth.com/api/v2/users/accountId/messages?messageStatus=DLR_EXPIRED&prevPage=GL83PD3C + prevPageToken: + type: string + description: The isolated pagination token for the previous page. + example: DLAPE902 + nextPageToken: + type: string + description: The isolated pagination token for the next page. + example: GL83PD3C + messagingRequestError: + title: MessagingRequestError + type: object + properties: + type: + type: string + description: + type: string + required: + - type + - description + createMessageRequestError: + title: CreateMessageRequestError + type: object + properties: + type: + type: string + description: + type: string + fieldErrors: + type: array + items: + $ref: "#/components/schemas/fieldError" + required: + - type + - description + messageId: + type: string + description: The ID of the message. + example: 1589228074636lm4k2je7j7jklbn2 + media: + title: Media + type: object + properties: + content: + type: string + contentLength: + type: integer + mediaName: + type: string + segmentCount: + type: integer + description: The number of segments the user's message is broken into before sending over carrier networks. + example: 1 + tag: + title: Tag + type: string + description: A custom string that will be included in callback events of the message. Max 1024 characters. + example: custom string + expiration: + type: string + format: date-time + description: >- + A string with the date/time value that the message will automatically + expire by. This must be a valid RFC-3339 value, e.g., + 2021-03-14T01:59:26Z or 2021-03-13T20:59:26-05:00. Must be a date-time in the future. + example: "2021-02-01T11:29:18-05:00" + carrierName: + type: string + description: |- + The name of the Authorized Message Provider (AMP) that handled this message. + In the US, this is the carrier that the message was sent to. + This field is present only when this account feature has been enabled. + example: AT&T + message: + title: Message + type: object + properties: + id: + type: string + description: The id of the message. + example: 1589228074636lm4k2je7j7jklbn2 + owner: + type: string + description: The Bandwidth phone number associated with the message. + example: "+15554443333" + applicationId: + $ref: "#/components/schemas/applicationId" + time: + type: string + format: date-time + description: The datetime stamp of the message in ISO 8601 + example: 2024-12-02T20:15:57.278201Z + segmentCount: + $ref: "#/components/schemas/segmentCount" + direction: + $ref: "#/components/schemas/messageDirectionEnum" + to: + uniqueItems: true + type: array + items: + type: string + description: The phone number recipients of the message. + example: + - "+15552223333" + from: + type: string + description: The phone number the message was sent from. + example: "+15553332222" + media: + uniqueItems: true + type: array + items: + type: string + description: >- + The list of media URLs sent in the message. Including a `filename` + field in the `Content-Disposition` header of the media linked with + a URL will set the displayed file name. This is a best practice to + ensure that your media has a readable file name. + example: + - https://dev.bandwidth.com/images/bandwidth-logo.png + text: + type: string + description: The contents of the message. + example: Hello world + tag: + $ref: "#/components/schemas/tag" + priority: + $ref: "#/components/schemas/priorityEnum" + expiration: + $ref: "#/components/schemas/expiration" + messageRequest: + title: MessageRequest + type: object + required: + - applicationId + - to + - from + properties: + applicationId: + $ref: "#/components/schemas/applicationId" + to: + uniqueItems: true + type: array + description: The phone number(s) the message should be sent to in E164 format. + example: + - "+15554443333" + - "+15552223333" + items: + type: string + from: + type: string + description: >- + Either an alphanumeric sender ID or the sender's Bandwidth phone number in E.164 format, which must be hosted within Bandwidth and linked to the account that is generating the message. + + Alphanumeric Sender IDs can contain up to 11 characters, upper-case letters A-Z, lower-case letters a-z, numbers 0-9, space, hyphen -, plus +, underscore _ and ampersand &. + Alphanumeric Sender IDs must contain at least one letter. + example: + "+15551113333" + text: + $ref: "#/components/schemas/messageText" + media: + $ref: "#/components/schemas/messageMedia" + tag: + $ref: "#/components/schemas/tag" + priority: + $ref: "#/components/schemas/priorityEnum" + expiration: + $ref: "#/components/schemas/expiration" + messageText: + type: string + description: The contents of the text message. Must be 2048 characters or less. + maxLength: 2048 + example: Hello world + messageMedia: + type: array + items: + type: string + format: uri + maxLength: 4096 + description: >- + A list of URLs to include as media attachments as part of the message. + + Each URL can be at most 4096 characters. + example: + - https://dev.bandwidth.com/images/bandwidth-logo.png + - https://dev.bandwidth.com/images/github_logo.png + createMultiChannelMessageResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + example: [] + data: + $ref: "#/components/schemas/multiChannelMessageResponseData" + errors: + type: array + items: + $ref: "#/components/schemas/errorObject" + example: [] + multiChannelError: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + example: [] + data: + type: object + nullable: true + example: null + errors: + type: array + items: + $ref: "#/components/schemas/errorObject" + link: + type: object + properties: + rel: + type: string + href: + type: string + errorObject: + type: object + properties: + type: + description: A concise summary of the error used for categorization. + type: string + description: + description: A detailed explanation of the error. + type: string + source: + $ref: "#/components/schemas/errorSource" + required: + - type + - description + - source + errorSource: + title: Error Source + type: object + description: Specifies relevant sources of the error, if any. + properties: + parameter: + type: string + description: The relevant URI query parameter causing the error + field: + type: string + description: The request body field that led to the error + header: + type: string + description: The header field that contributed to the error + reference: + type: string + description: A resource ID or path linked to the error + multiChannelMessageChannelEnum: + description: The channel of the multi-channel message. + type: string + enum: + - RBM + - SMS + - MMS + example: RBM + multiChannelSenderId: + type: string + description: The sender ID of the message. This could be an alphanumeric sender ID. + example: "BandwidthRBM" + multiChannelDestination: + type: string + description: The phone number the message should be sent to in E164 format. + example: "+15552223333" + multiChannelDestinations: + uniqueItems: true + type: array + description: The destination phone number(s) of the message, in E164 format. + example: + - "+15554443333" + items: + type: string + rbmMessageContentText: + title: RBM Text + type: object + properties: + text: + type: string + description: The text associated with the message. Must be 3270 characters or less + maxLength: 3270 + example: Hello world + suggestions: + $ref: "#/components/schemas/multiChannelFullActions" + required: + - text + rbmMediaHeightEnum: + type: string + description: The height of the media. + enum: + - SHORT + - MEDIUM + - TALL + example: SHORT + rbmMessageContentFile: + title: RBM Rich Media File + type: object + properties: + fileUrl: + type: string + format: uri + description: The URL of the media file. 100MB is the maximum file size. + example: https://dev.bandwidth.com/images/bandwidth-logo.png + maxLength: 1000 + thumbnailUrl: + type: string + format: uri + description: The URL of the thumbnail image. Applies only to video file media. + example: https://dev.bandwidth.com/images/bandwidth-logo.png + maxLength: 1000 + required: + - fileUrl + mmsMessageContentFile: + title: MMS Media File + type: object + properties: + fileUrl: + type: string + format: uri + description: >- + The URL of a media attachment. + + + For MMS, the API limits file size to 3.5MB. + Specific carriers and channels may have a smaller limit that could cause a large file to fail, see + [here](https://support.bandwidth.com/hc/en-us/articles/360014235473-What-are-the-MMS-file-size-limits) + for more details. + example: https://dev.bandwidth.com/images/bandwidth-logo.png + maxLength: 1000 + required: + - fileUrl + rbmMessageMedia: + title: RBM Media + type: object + properties: + media: + $ref: "#/components/schemas/rbmMessageContentFile" + suggestions: + $ref: "#/components/schemas/multiChannelFullActions" + required: + - media + rbmCardContent: + type: object + properties: + title: + type: string + description: The title of the card. Must be 200 characters or less. + maxLength: 200 + example: "Bandwidth" + description: + type: string + description: The description of the card. Must be 2000 characters or less. + maxLength: 2000 + example: "Bandwidth is a communications platform as a service (CPaaS) company." + media: + allOf: + - $ref: "#/components/schemas/rbmMessageContentFile" + - type: object + properties: + height: + $ref: "#/components/schemas/rbmMediaHeightEnum" + required: + - height + suggestions: + description: An array of suggested actions for the recipient that will be displayed on the rich card. + type: array + items: + $ref: "#/components/schemas/multiChannelAction" + maxItems: 4 + rbmStandaloneCard: + title: Standalone Card + type: object + properties: + orientation: + $ref: "#/components/schemas/standaloneCardOrientationEnum" + thumbnailImageAlignment: + $ref: "#/components/schemas/thumbnailAlignmentEnum" + cardContent: + $ref: "#/components/schemas/rbmCardContent" + suggestions: + $ref: "#/components/schemas/multiChannelFullActions" + required: + - orientation + - thumbnailImageAlignment + - cardContent + standaloneCardOrientationEnum: + type: string + enum: + - HORIZONTAL + - VERTICAL + example: VERTICAL + thumbnailAlignmentEnum: + type: string + description: >- + The alignment of the thumbnail image in the card. Only applicable if + the card using horizontal orientation. + enum: + - LEFT + - RIGHT + example: LEFT + rbmMessageCarouselCard: + title: Carousel + type: object + properties: + cardWidth: + $ref: "#/components/schemas/cardWidthEnum" + cardContents: + type: array + items: + $ref: "#/components/schemas/rbmCardContent" + maxItems: 10 + suggestions: + $ref: "#/components/schemas/multiChannelFullActions" + required: + - cardContents + - cardWidth + cardWidthEnum: + type: string + enum: + - SMALL + - MEDIUM + example: SMALL + smsMessageContent: + title: SMS Text + type: object + properties: + text: + $ref: "#/components/schemas/messageText" + required: + - text + mmsMessageContent: + title: MMS Message + type: object + properties: + text: + $ref: "#/components/schemas/messageText" + media: + type: array + items: + $ref: "#/components/schemas/mmsMessageContentFile" + rbmMessageContentRichCard: + title: RBM Rich Card + oneOf: + - $ref: "#/components/schemas/rbmStandaloneCard" + - $ref: "#/components/schemas/rbmMessageCarouselCard" + rbmActionTypeEnum: + type: string + enum: + - REPLY + - DIAL_PHONE + - SHOW_LOCATION + - CREATE_CALENDAR_EVENT + - OPEN_URL + - REQUEST_LOCATION + example: REPLY + rbmActionText: + title: Text + type: string + description: Displayed text for user to click + maxLength: 25 + example: Hello world + rbmActionPostbackData: + title: Post Back Data + type: string + format: byte + description: Base64 payload the customer receives when the reply is clicked. + maxLength: 2048 + example: SGVsbG8gd29ybGQ= + rbmActionBase: + type: object + properties: + type: + $ref: '#/components/schemas/rbmActionTypeEnum' + text: + $ref: "#/components/schemas/rbmActionText" + postbackData: + $ref: "#/components/schemas/rbmActionPostbackData" + required: + - text + - postbackData + - type + rbmActionDial: + allOf: + - $ref: "#/components/schemas/rbmActionBase" + - title: Dial Phone + type: object + properties: + phoneNumber: + type: string + description: The phone number to dial. Must be E164 format. + example: "+15552223333" + required: + - phoneNumber + rbmActionViewLocation: + allOf: + - $ref: "#/components/schemas/rbmActionBase" + - title: Show Location + type: object + properties: + latitude: + type: string + format: double + description: The latitude of the location. + example: "37.7749" + longitude: + type: string + format: double + description: The longitude of the location. + example: "-122.4194" + label: + type: string + description: The label of the location. + example: "San Francisco" + maxLength: 100 + required: + - latitude + - longitude + multiChannelActionCalendarEvent: + allOf: + - $ref: "#/components/schemas/rbmActionBase" + - title: Calendar Event + type: object + properties: + title: + type: string + description: The title of the event. + example: "Meeting with John" + maxLength: 100 + startTime: + type: string + format: date-time + description: The start time of the event. + example: 2022-09-14T18:20:16.000Z + endTime: + type: string + format: date-time + description: The end time of the event. + example: 2022-09-14T18:20:16.000Z + description: + type: string + description: The description of the event. + example: "Discuss the new project" + maxLength: 500 + required: + - title + - startTime + - endTime + rbmActionOpenUrl: + allOf: + - $ref: "#/components/schemas/rbmActionBase" + - title: Open URL + type: object + properties: + url: + type: string + format: uri + description: The URL to open in browser. + example: https://dev.bandwidth.com + maxLength: 2048 + required: + - url + multiChannelFullActions: + type: array + description: An array of suggested actions for the recipient. + items: + $ref: "#/components/schemas/multiChannelAction" + maxItems: 11 + multiChannelAction: + oneOf: + - $ref: "#/components/schemas/rbmActionBase" + - $ref: "#/components/schemas/rbmActionDial" + - $ref: "#/components/schemas/rbmActionViewLocation" + - $ref: "#/components/schemas/multiChannelActionCalendarEvent" + - $ref: "#/components/schemas/rbmActionOpenUrl" + discriminator: + propertyName: type + mapping: + REPLY: "#/components/schemas/rbmActionBase" + DIAL_PHONE: "#/components/schemas/rbmActionDial" + SHOW_LOCATION: "#/components/schemas/rbmActionViewLocation" + CREATE_CALENDAR_EVENT: "#/components/schemas/multiChannelActionCalendarEvent" + OPEN_URL: "#/components/schemas/rbmActionOpenUrl" + REQUEST_LOCATION: "#/components/schemas/rbmActionBase" + multiChannelChannelListObject: + type: object + properties: + from: + $ref: "#/components/schemas/multiChannelSenderId" + applicationId: + $ref: "#/components/schemas/applicationId" + channel: + $ref: "#/components/schemas/multiChannelMessageChannelEnum" + content: + description: The content of the message. + oneOf: + - $ref: "#/components/schemas/rbmMessageContentText" + - $ref: "#/components/schemas/rbmMessageMedia" + - $ref: "#/components/schemas/rbmMessageContentRichCard" + - $ref: '#/components/schemas/smsMessageContent' + - $ref: '#/components/schemas/mmsMessageContent' + required: + - from + - applicationId + - channel + - content + multiChannelMessageRequest: + description: Multi-Channel Message Request + type: object + properties: + to: + $ref: "#/components/schemas/multiChannelDestination" + channelList: + type: array + description: A list of message bodies. The messages will be attempted in the order they are listed. Once a message sends successfully, the others will be ignored. + items: + $ref: "#/components/schemas/multiChannelChannelListObject" + maxItems: 4 + tag: + $ref: "#/components/schemas/tag" + priority: + $ref: "#/components/schemas/priorityEnum" + expiration: + $ref: "#/components/schemas/expiration" + required: + - to + - channelList + multiChannelMessageResponseData: + description: The data returned in a multichannel message response. + type: object + properties: + messageId: + $ref: "#/components/schemas/messageId" + time: + description: The time the message was received by the Bandwidth API. + type: string + format: date-time + example: 2025-01-01T18:20:16.000000Z + direction: + $ref: "#/components/schemas/messageDirectionEnum" + to: + $ref: "#/components/schemas/multiChannelDestinations" + channelList: + type: array + description: A list of message bodies. The messages will be attempted in the order they are listed. Once a message sends successfully, the others will be ignored. + items: + allOf: + - $ref: "#/components/schemas/multiChannelChannelListObject" + - type: object + properties: + owner: + type: string + description: The Bandwidth senderId associated with the message. Identical to 'from'. + required: + - owner + maxItems: 4 + tag: + $ref: "#/components/schemas/tag" + priority: + $ref: "#/components/schemas/priorityEnum" + expiration: + $ref: "#/components/schemas/expiration" + required: + - messageId + - time + - direction + - to + - channelList + multiChannelMessageContent: + description: The structure of the content field of a multichannel message. + type: object + properties: + text: + type: string + media: + $ref: '#/components/schemas/rbmMessageContentFile' + rbmSuggestionResponse: + type: object + properties: + text: + type: string + description: The text associated with the suggestion response. + example: "Yes, I would like to proceed" + postbackData: + $ref: "#/components/schemas/rbmActionPostbackData" + rbmLocationResponse: + type: object + properties: + latitude: + type: string + format: double + description: The latitude of the client's location. + example: "37.7749" + longitude: + type: string + format: double + description: The longitude of the client's location. + example: "-122.4194" + callback: + description: |- + Callbacks are divided into two types based on direction of the related message: + - `statusCallback` indicates status of an outbound MT SMS, MMS, or RBM message. + - `inboundCallback` indicates an inbound MO message or a multichannel message client's response to a suggestion or location request. + type: object + oneOf: + - $ref: '#/components/schemas/statusCallback' + - $ref: '#/components/schemas/inboundCallback' + discriminator: + propertyName: type + mapping: + message-sent: '#/components/schemas/statusCallback' + message-delivered: '#/components/schemas/statusCallback' + message-failed: '#/components/schemas/statusCallback' + message-read: '#/components/schemas/statusCallback' + message-received: '#/components/schemas/inboundCallback' + request-location-response: '#/components/schemas/inboundCallback' + suggestion-response: '#/components/schemas/inboundCallback' + statusCallback: + type: object + description: Represents a status callback for an outbound MT SMS or MMS or RBM message. + properties: + time: + type: string + format: date-time + example: 2024-12-02T20:15:57.278201Z + eventTime: + type: string + description: Represents the time at which the message was read, for `message-read` callbacks. + format: date-time + example: 2024-12-02T20:15:58.278205Z + type: + $ref: "#/components/schemas/statusCallbackTypeEnum" + to: + type: string + description: |- + The destination phone number the message was sent to. + For status callbacks, this the the Bandwidth user's client phone number. + example: "+15552223333" + description: + type: string + description: A detailed description of the event described by the callback. + example: Message delivered to carrier. + message: + $ref: '#/components/schemas/statusCallbackMessage' + errorCode: + type: integer + description: Optional error code, applicable only when type is `message-failed`. + example: 4405 + carrierName: + $ref: "#/components/schemas/carrierName" + required: + - time + - type + - to + - description + - message + inboundCallback: + type: object + description: Represents an inbound callback. + properties: + time: + type: string + format: date-time + example: 2024-12-02T20:15:57.278201Z + type: + $ref: "#/components/schemas/inboundCallbackTypeEnum" + to: + type: string + description: | + The destination phone number the message was sent to. + For inbound callbacks, this is the Bandwidth number or alphanumeric identifier that received the message. + example: "+15552223333" + description: + type: string + description: A detailed description of the event described by the callback. + example: Incoming message received + message: + $ref: '#/components/schemas/inboundCallbackMessage' + carrierName: + $ref: "#/components/schemas/carrierName" + required: + - time + - type + - to + - description + - message + statusCallbackTypeEnum: + type: string + description: |- + The possible status callbacks when sending an MT SMS or MMS or RBM message: + - `message-sending` indicates that Bandwidth is sending the message to the upstream provider. + - `message-delivered` indicates that the message was successfully sent. + - `message-failed` indicates that the message could not be sent to the intended recipient. + - `message-read` indicates that the RBM message was read by the recipient. + enum: + - message-sending + - message-delivered + - message-failed + - message-read + example: message-delivered + inboundCallbackTypeEnum: + type: string + description: |- + The possible inbound callback types originating from MO messages or multichannel message client responses: + - `message-received` indicates an MO message from a Bandwidth user's client to a Bandwidth number. + - `request-location-response` indicates a response to a location request sent by the Bandwidth user's client after receiving an RBM message. + - `suggestion-response` indicates a response to a suggestion sent by the Bandwidth user's client after receiving an RBM message. + enum: + - message-received + - request-location-response + - suggestion-response + example: message-received + statusCallbackMessage: + description: Message payload schema within a callback + type: object + properties: + id: + type: string + description: A unique identifier of the message. + example: 1661365814859loidf7mcwd4qacn7 + owner: + type: string + description: The Bandwidth phone number or alphanumeric identifier associated with the message. + example: "+15553332222" + applicationId: + $ref: "#/components/schemas/applicationId" + time: + type: string + format: date-time + example: 2024-12-02T20:15:57.666000Z + segmentCount: + $ref: "#/components/schemas/segmentCount" + direction: + $ref: "#/components/schemas/messageDirectionEnum" + to: + description: The phone number recipients of the message. + uniqueItems: true + type: array + items: + type: string + example: + - "+15552223333" + from: + type: string + description: The Bandwidth phone number or alphanumeric identifier the message was sent from. + example: "+15553332222" + text: + type: string + example: Hello world + tag: + $ref: "#/components/schemas/tag" + media: + type: array + description: Optional media, not applicable for sms + items: + type: string + format: uri + example: + - "https://dev.bandwidth.com/images/bandwidth-logo.png" + - "https://dev.bandwidth.com/images/github_logo.png" + priority: + $ref: "#/components/schemas/priorityEnum" + channel: + $ref: "#/components/schemas/multiChannelMessageChannelEnum" + required: + - id + - owner + - applicationId + - time + - segmentCount + - direction + - to + - from + inboundCallbackMessage: + allOf: + - $ref: "#/components/schemas/statusCallbackMessage" + - type: object + properties: + content: + $ref: "#/components/schemas/multiChannelMessageContent" + suggestionResponse: + $ref: "#/components/schemas/rbmSuggestionResponse" + locationResponse: + $ref: "#/components/schemas/rbmLocationResponse" + required: + - id + - owner + - applicationId + - time + - segmentCount + - direction + - to + - from + requestBodies: + createMessageRequest: + content: + application/json: + schema: + $ref: "#/components/schemas/messageRequest" + required: true + createMultiChannelMessageRequest: + content: + application/json: + schema: + $ref: "#/components/schemas/multiChannelMessageRequest" + required: true + uploadMediaRequest: + content: + application/json: + schema: + type: string + format: binary + application/ogg: + schema: + type: string + format: binary + application/pdf: + schema: + type: string + format: binary + application/rtf: + schema: + type: string + format: binary + application/zip: + schema: + type: string + format: binary + application/x-tar: + schema: + type: string + format: binary + application/xml: + schema: + type: string + format: binary + application/gzip: + schema: + type: string + format: binary + application/x-bzip2: + schema: + type: string + format: binary + application/x-gzip: + schema: + type: string + format: binary + application/smil: + schema: + type: string + format: binary + application/javascript: + schema: + type: string + format: binary + audio/mp4: + schema: + type: string + format: binary + audio/mpeg: + schema: + type: string + format: binary + audio/ogg: + schema: + type: string + format: binary + audio/flac: + schema: + type: string + format: binary + audio/webm: + schema: + type: string + format: binary + audio/wav: + schema: + type: string + format: binary + audio/amr: + schema: + type: string + format: binary + audio/3gpp: + schema: + type: string + format: binary + image/bmp: + schema: + type: string + format: binary + image/gif: + schema: + type: string + format: binary + image/heic: + schema: + type: string + format: binary + image/heif: + schema: + type: string + format: binary + image/jpeg: + schema: + type: string + format: binary + image/pjpeg: + schema: + type: string + format: binary + image/png: + schema: + type: string + format: binary + image/svg+xml: + schema: + type: string + format: binary + image/tiff: + schema: + type: string + format: binary + image/webp: + schema: + type: string + format: binary + image/x-icon: + schema: + type: string + format: binary + text/css: + schema: + type: string + format: binary + text/csv: + schema: + type: string + format: binary + text/calendar: + schema: + type: string + format: binary + text/html: + schema: + type: string + format: binary + text/plain: + schema: + type: string + format: binary + text/javascript: + schema: + type: string + format: binary + text/vcard: + schema: + type: string + format: binary + text/vnd.wap.wml: + schema: + type: string + format: binary + text/xml: + schema: + type: string + format: binary + video/avi: + schema: + type: string + format: binary + video/mp4: + schema: + type: string + format: binary + video/mpeg: + schema: + type: string + format: binary + video/ogg: + schema: + type: string + format: binary + video/quicktime: + schema: + type: string + format: binary + video/webm: + schema: + type: string + format: binary + video/x-ms-wmv: + schema: + type: string + format: binary + video/x-flv: + schema: + type: string + format: binary + required: true + responses: + createMessageResponse: + description: Accepted + content: + application/json: + schema: + $ref: "#/components/schemas/message" + createMultiChannelMessageResponse: + description: Accepted + content: + application/json: + schema: + $ref: "#/components/schemas/createMultiChannelMessageResponse" + listMessagesResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/messagesList" + getMediaResponse: + description: OK + content: + application/octet-stream: + schema: + type: string + description: Successful Operation + format: binary + listMediaResponse: + description: OK + headers: + Continuation-Token: + description: Continuation token used to retrieve subsequent media. + schema: + type: string + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/media" + messagingBadRequestError: + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/messagingRequestError" + messagingNotAcceptableError: + description: Not Acceptable + content: + application/json: + schema: + $ref: "#/components/schemas/messagingRequestError" + createMessageBadRequestError: + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/createMessageRequestError" + messagingUnauthorizedError: + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/messagingRequestError" + messagingForbiddenError: + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/messagingRequestError" + messagingNotFoundError: + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/messagingRequestError" + messagingMethodNotAllowedError: + description: Method Not Allowed + content: + application/json: + schema: + $ref: "#/components/schemas/messagingRequestError" + messagingInvalidMediaTypeError: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: "#/components/schemas/messagingRequestError" + messagingTooManyRequestsError: + description: Too Many Requests + content: + application/json: + schema: + $ref: "#/components/schemas/messagingRequestError" + messagingInternalServerError: + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/messagingRequestError" + multiChannelBadRequestError: + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/multiChannelError" + example: + links: [ ] + data: null + errors: + - type: "request-validation" + description: "The 'channelList[0].from' field must contain exactly one telephone number" + source: + field: "channelList[0].from" + multiChannelNotAcceptableError: + description: Not Acceptable + # Unsurprisingly, if the accept header is bad spring has trouble generating a response; could be fixed. + multiChannelUnauthorizedError: + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/multiChannelError" + example: + links: [ ] + data: null + errors: + - type: "unauthorized" + description: "Authentication Failed" + source: { } + multiChannelForbiddenError: + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/multiChannelError" + example: + links: [ ] + data: null + errors: + - type: "forbidden" + description: "Access Denied" + source: { } + multiChannelNotFoundError: + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/multiChannelError" + example: + links: [ ] + data: null + errors: + - type: "forbidden" + description: "Resource not found." + source: { } + multiChannelMethodNotAllowedError: + description: Method Not Allowed + content: + application/json: + schema: + $ref: "#/components/schemas/multiChannelError" + example: + links: [ ] + data: null + errors: + - type: "method-not-allowed" + description: "Method 'PUT' not supported for this resource." + source: { } + multiChannelInvalidMediaTypeError: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: "#/components/schemas/multiChannelError" + example: + links: [ ] + data: null + errors: + - type: "unsupported-content-type" + description: "Content-Type 'application/xml;charset=UTF-8' is not supported. Please use 'application/json'" + source: + header: "Content-Type" + multiChannelTooManyRequestsError: + description: Too Many Requests + content: + application/json: + schema: + $ref: "#/components/schemas/multiChannelError" + example: + links: [ ] + data: null + errors: + - type: "rate-limit-exceeded" + description: "You have exceeded your rate limit for this endpoint. Please retry later." + source: { } + multiChannelInternalServerError: + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/multiChannelError" + example: + links: [ ] + data: null + errors: + - type: "internal-server-error" + description: "Internal server error. No further information available" + source: { } + callbacks: + inboundCallback: + "{inboundCallbackUrl}": + post: + requestBody: + required: true + description: |- +

This Inbound Message Webhook is an envelope containing either a received (MO) message to your + message-enabled Bandwidth telephone number or a multichannel client's response to a suggestion response + or location request. +

The payload type will be one of message-received, suggestion-response, or location-request-response. +

Note that suggestion-response and location-request-response callback types are pertinent only for RBM messages sent from the /messages/multiChannel endpoint. +

Please visit Webhooks

+ content: + application/json: + schema: + $ref: "#/components/schemas/inboundCallback" + examples: + smsMessageReceivedCallback: + $ref: '#/components/examples/smsMessageReceivedCallbackExample' + mmsMessageReceivedCallback: + $ref: '#/components/examples/mmsMessageReceivedCallbackExample' + responses: + "200": + description: OK + "202": + description: Accepted + statusCallback: + "{statusCallbackUrl}": + post: + requestBody: + required: true + description: |- +

This Outbound Message Webhook is an envelope containing status information regarding a message sent (MT) + from your message-enabled Bandwidth telephone number. +

The payload type will be one of message-sending, message-delivered, message-failed or message-read. +

Note that message-read callbacks are pertinent only for RBM messages sent from the /messages/multiChannel endpoint. +

Please visit Webhooks

+ content: + application/json: + schema: + $ref: "#/components/schemas/statusCallback" + examples: + messageSendingCallback: + $ref: '#/components/examples/messageSendingCallbackExample' + smsMessageDeliveredCallback: + $ref: '#/components/examples/smsMessageDeliveredCallbackExample' + mmsMessageDeliveredCallback: + $ref: '#/components/examples/mmsMessageDeliveredCallbackExample' + groupMmsMessageDeliveredCallback: + $ref: '#/components/examples/groupMmsMessageDeliveredCallbackExample' + messageFailedCallback: + $ref: '#/components/examples/messageFailedCallbackExample' + responses: + "200": + description: OK + "202": + description: Accepted + securitySchemes: + Basic: + type: http + scheme: basic + description: |- + Basic authentication is a simple authentication scheme built into the HTTP protocol. To use it, send your HTTP requests with an `Authorization` header that contains the word `Basic` followed by a space and a Base64-encoded string `username:password`. + + - Example: `Authorization: Basic ZGVtbZpwQDU1dzByZA==` + examples: + smsMessageReceivedCallbackExample: + summary: An example of a sms message-received callback body. + value: + time: "2025-01-06T15:43:35.502180Z" + type: message-received + to: "+12345678902" + description: Incoming message received + message: + id: "14762070468292kw2fuqty55yp2b2" + owner: "+12345678902" + applicationId: "93de2206-9669-4e07-948d-329f4b722ee2" + time: "2025-01-06T15:43:34.000000Z" + segmentCount: 1 + direction: in + to: + - "+12345678902" + from: "+12345678901" + text: "Hey, check out this SMS!" + mmsMessageReceivedCallbackExample: + summary: An example of a mms message-received callback body. + value: + time: "2024-09-14T18:20:45.160744Z" + type: message-received + to: "+12345678902" + description: Incoming message received + message: + id: "14762070468292kw2fuqty55yp2b2" + owner: "+12345678902" + applicationId: "93de2206-9669-4e07-948d-329f4b722ee2" + time: "2024-09-14T18:20:45.160744Z" + segmentCount: 1 + direction: in + to: + - "+12345678902" + - "+12345678903" + from: "+12345678901" + text: "Hey, check out the MMS!" + media: + - "https://messaging.bandwidth.com/api/v2/users/9900902/media/14762070468292kw2fuqty55yp2b2/0/bw.png" + messageSendingCallbackExample: + summary: An example of a message-sending callback body. + value: + time: "2024-06-25T18:42:36.979456Z" + type: message-sending + to: "+15554443333" + description: Message is sending to carrier. + message: + id: "1593110555875xo7watq5px6rbe5d" + owner: "+15552221111" + applicationId: "cfd4fb83-7531-4acc-b471-42d0bb76a65c" + time: "2024-06-25T18:42:35.876906Z" + segmentCount: 1 + direction: out + to: + - "+15554443333" + from: "+15552221111" + text: "" + media: + - "https://dev.bandwidth.com/images/bandwidth-logo.png" + tag: your tag here + smsMessageDeliveredCallbackExample: + summary: An example of a sms message-delivered callback body. + value: + type: message-delivered + time: "2024-09-14T18:20:11.160744Z" + description: Message delivered to carrier. + to: "+12345678902" + message: + id: "14762070468292kw2fuqty55yp2b2" + time: "2024-09-14T18:20:11.160744Z" + to: + - "+12345678902" + from: "+12345678901" + text: "" + applicationId: "93de2206-9669-4e07-948d-329f4b722ee2" + owner: "+12345678902" + direction: out + segmentCount: 1 + mmsMessageDeliveredCallbackExample: + summary: An example of a mms message-delivered callback body. + value: + type: message-delivered + time: "2024-09-14T18:20:24.160544Z" + description: Message delivered to carrier. + to: "+12345678902" + message: + id: "14762070468292kw2fuqty55yp2b2" + time: "2024-09-14T18:20:24.160544Z" + to: + - "+12345678902" + from: "+12345678901" + text: "" + applicationId: "93de2206-9669-4e07-948d-329f4b722ee2" + owner: "+12345678902" + direction: out + segmentCount: 1 + media: + - "https://dev.bandwidth.com/images/bandwidth-logo.png" + groupMmsMessageDeliveredCallbackExample: + summary: An example of a group mms message-delivered callback body. + value: + type: message-delivered + time: "2024-09-14T18:20:17.160544Z" + description: Message delivered to carrier. + to: "+12345678902" + message: + id: "14762070468292kw2fuqty55yp2b2" + time: "2024-09-14T18:20:17.160544Z" + to: + - "+12345678902" + - "+12345678903" + from: "+12345678901" + text: "" + applicationId: "93de2206-9669-4e07-948d-329f4b722ee2" + owner: "+12345678902" + direction: out + segmentCount: 1 + messageFailedCallbackExample: + summary: An example of a message-failed callback body. + value: + type: message-failed + time: "2024-12-18T16:51:27.704450Z" + description: forbidden to country + to: "+52345678903" + errorCode: 4432 + message: + id: "14762070468292kw2fuqty55yp2b2" + time: "2024-12-18T16:51:27.704450Z" + to: + - "+12345678902" + - "+52345678903" + from: "+12345678901" + text: "" + applicationId: "93de2206-9669-4e07-948d-329f4b722ee2" + media: + - "https://dev.bandwidth.com/images/bandwidth-logo.png" + owner: "+12345678901" + direction: out + segmentCount: 1 +security: + - Basic: [] +tags: + - name: Messages + - name: Media + - name: Multi-Channel diff --git a/test/fixtures/multi-factor-auth.yml b/test/fixtures/multi-factor-auth.yml new file mode 100644 index 0000000..acc50f2 --- /dev/null +++ b/test/fixtures/multi-factor-auth.yml @@ -0,0 +1,311 @@ +openapi: 3.0.3 +info: + title: Multi-Factor Authentication + description: |- + Bandwidth's Two-Factor Authentication service + + ## Base Path + + https://mfa.bandwidth.com/api/v1 + contact: + name: Bandwidth Support + email: support@bandwidth.com + url: https://support.bandwidth.com + termsOfService: https://www.bandwidth.com/legal/terms-of-use-bandwidthcom-web-sites/ + version: 3.1.0 +servers: + - url: https://mfa.bandwidth.com/api/v1 + description: Production +paths: + /accounts/{accountId}/code/voice: + post: + tags: + - MFA + summary: Voice Authentication Code + description: Send an MFA Code via a phone call. + operationId: generateVoiceCode + parameters: + - $ref: '#/components/parameters/accountId' + requestBody: + $ref: '#/components/requestBodies/codeRequest' + responses: + '200': + $ref: '#/components/responses/voiceCodeResponse' + '400': + $ref: '#/components/responses/mfaBadRequestError' + '401': + $ref: '#/components/responses/mfaUnauthorizedError' + '403': + $ref: '#/components/responses/mfaForbiddenError' + '500': + $ref: '#/components/responses/mfaInternalServerError' + /accounts/{accountId}/code/messaging: + post: + tags: + - MFA + summary: Messaging Authentication Code + description: Send an MFA code via text message (SMS). + operationId: generateMessagingCode + parameters: + - $ref: '#/components/parameters/accountId' + requestBody: + $ref: '#/components/requestBodies/codeRequest' + responses: + '200': + $ref: '#/components/responses/messagingCodeResponse' + '400': + $ref: '#/components/responses/mfaBadRequestError' + '401': + $ref: '#/components/responses/mfaUnauthorizedError' + '403': + $ref: '#/components/responses/mfaForbiddenError' + '500': + $ref: '#/components/responses/mfaInternalServerError' + /accounts/{accountId}/code/verify: + post: + tags: + - MFA + summary: Verify Authentication Code + description: Verify a previously sent MFA code. + operationId: verifyCode + parameters: + - $ref: '#/components/parameters/accountId' + requestBody: + $ref: '#/components/requestBodies/codeVerify' + responses: + '200': + $ref: '#/components/responses/verifyCodeResponse' + '400': + $ref: '#/components/responses/mfaBadRequestError' + '401': + $ref: '#/components/responses/mfaUnauthorizedError' + '403': + $ref: '#/components/responses/mfaForbiddenError' + '429': + $ref: '#/components/responses/mfaTooManyRequestsError' + '500': + $ref: '#/components/responses/mfaInternalServerError' +components: + schemas: + codeRequest: + type: object + properties: + to: + type: string + description: The phone number to send the mfa code to. + pattern: '^\+[1-9]\d{1,14}$' + example: '+19195551234' + from: + type: string + description: The application phone number, the sender of the mfa code. + pattern: '^\+[1-9]\d{1,14}$' + maxLength: 32 + example: '+19195554321' + applicationId: + type: string + description: The application unique ID, obtained from Bandwidth. + maxLength: 50 + example: 66fd98ae-ac8d-a00f-7fcd-ba3280aeb9b1 + scope: + type: string + description: >- + An optional field to denote what scope or action the mfa code is + addressing. If not supplied, defaults to "2FA". + maxLength: 25 + example: 2FA + message: + type: string + description: >- + The message format of the mfa code. There are three values that the + system will replace "{CODE}", "{NAME}", "{SCOPE}". The "{SCOPE}" + and "{NAME} value template are optional, while "{CODE}" must be + supplied. As the name would suggest, code will be replace with the + actual mfa code. Name is replaced with the application name, + configured during provisioning of mfa. The scope value is the same + value sent during the call and partitioned by the server. + maxLength: 2048 + example: 'Your temporary {NAME} {SCOPE} code is {CODE}' + digits: + type: integer + description: >- + The number of digits for your mfa code. The valid number ranges + from 2 to 8, inclusively. + minimum: 4 + maximum: 8 + example: 6 + required: + - to + - from + - applicationId + - message + - digits + voiceCodeResponse: + type: object + properties: + callId: + type: string + description: Programmable Voice API Call ID. + example: c-15ac29a2-1331029c-2cb0-4a07-b215-b22865662d85 + messagingCodeResponse: + type: object + properties: + messageId: + type: string + description: Messaging API Message ID. + example: 1589228074636lm4k2je7j7jklbn2 + verifyCodeRequest: + type: object + properties: + to: + type: string + description: The phone number to send the mfa code to. + pattern: '^\+[1-9]\d{1,14}$' + example: '+19195551234' + scope: + type: string + description: >- + An optional field to denote what scope or action the mfa code is + addressing. If not supplied, defaults to "2FA". + example: 2FA + expirationTimeInMinutes: + type: number + description: >- + The time period, in minutes, to validate the mfa code. By setting + this to 3 minutes, it will mean any code generated within the last 3 + minutes are still valid. The valid range for expiration time is + between 0 and 15 minutes, exclusively and inclusively, respectively. + minimum: 1 + maximum: 15 + example: 3 + code: + type: string + description: The generated mfa code to check if valid. + minLength: 4 + maxLength: 8 + example: '123456' + required: + - to + - expirationTimeInMinutes + - code + verifyCodeResponse: + type: object + properties: + valid: + type: boolean + description: Whether or not the supplied code is valid. + example: true + mfaRequestError: + type: object + properties: + error: + type: string + description: A message describing the error with your request. + example: 400 Request is malformed or invalid + requestId: + type: string + description: The associated requestId from AWS. + example: 354cc8a3-6701-461e-8fa7-8671703dd898 + mfaUnauthorizedRequestError: + type: object + properties: + message: + type: string + description: Unauthorized + example: Unauthorized + mfaForbiddenRequestError: + type: object + properties: + message: + type: string + description: The message containing the reason behind the request being forbidden. + example: Missing Authentication Token + responses: + voiceCodeResponse: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/voiceCodeResponse' + messagingCodeResponse: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/messagingCodeResponse' + verifyCodeResponse: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/verifyCodeResponse' + mfaBadRequestError: + description: Bad Request + headers: {} + content: + application/json: + schema: + $ref: '#/components/schemas/mfaRequestError' + mfaUnauthorizedError: + description: Unauthorized + headers: {} + content: + application/json: + schema: + $ref: '#/components/schemas/mfaUnauthorizedRequestError' + mfaForbiddenError: + description: Forbidden + headers: {} + content: + application/json: + schema: + $ref: '#/components/schemas/mfaForbiddenRequestError' + mfaTooManyRequestsError: + description: Too Many Requests + headers: {} + content: + application/json: + schema: + $ref: '#/components/schemas/mfaRequestError' + mfaInternalServerError: + description: Internal Server Error + headers: {} + content: + application/json: + schema: + $ref: '#/components/schemas/mfaRequestError' + parameters: + accountId: + in: path + name: accountId + required: true + schema: + type: string + description: Your Bandwidth Account ID. + example: "9900000" + requestBodies: + codeRequest: + description: MFA code request body. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/codeRequest' + codeVerify: + description: MFA code verify request body. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/verifyCodeRequest' + securitySchemes: + Basic: + type: http + scheme: basic + description: >- + Basic authentication is a simple authentication scheme built into the HTTP protocol. To use it, send your HTTP requests with an Authorization header that contains the word Basic followed by a space and a base64-encoded string `username:password`. + + Example: `Authorization: Basic ZGVtbZpwQDU1dzByZA==` +security: + - Basic: [] +tags: + - name: MFA diff --git a/test/fixtures/no-servers.yml b/test/fixtures/no-servers.yml new file mode 100644 index 0000000..17e7054 --- /dev/null +++ b/test/fixtures/no-servers.yml @@ -0,0 +1,57 @@ +openapi: 3.0.3 +info: + title: Multi-Factor Authentication + description: Bandwidth's Two-Factor Authentication service + contact: + name: Bandwidth Support + email: support@bandwidth.com + url: https://support.bandwidth.com + termsOfService: https://www.bandwidth.com/legal/terms-of-use-bandwidthcom-web-sites/ + version: 3.1.0 +paths: + /code/verify: + post: + tags: + - MFA + summary: Verify Authentication Code + description: Verify a previously sent MFA code. + operationId: verifyCode + requestBody: + $ref: '#/components/requestBodies/codeVerify' + responses: + '200': + $ref: '#/components/responses/verifyCodeResponse' +components: + schemas: + verifyCodeRequest: + type: object + properties: + code: + type: string + description: The generated mfa code to check if valid. + example: '123456' + required: + - code + verifyCodeResponse: + type: object + properties: + valid: + type: boolean + description: Whether or not the supplied code is valid. + responses: + verifyCodeResponse: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/verifyCodeResponse' + requestBodies: + codeVerify: + description: MFA code verify request body. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/verifyCodeRequest' +tags: + - name: MFA diff --git a/test/fixtures/phone-number-lookup.yml b/test/fixtures/phone-number-lookup.yml new file mode 100644 index 0000000..5a4018f --- /dev/null +++ b/test/fixtures/phone-number-lookup.yml @@ -0,0 +1,424 @@ +openapi: 3.0.3 +info: + title: Phone Number Lookup + version: 1.1.0 + contact: + name: Bandwidth Support + email: support@bandwidth.com + url: https://support.bandwidth.com + termsOfService: https://www.bandwidth.com/legal/terms-of-use-bandwidthcom-web-sites/ + description: >- + A Bandwidth API to provide carrier information for a telephone number or + batch of telephone numbers. Currently supports lookups of telephone numbers + in the mainland United States, Alaska, Hawaii, and the District of Columbia. Telephone numbers submitted must be in E.164 format to + be processed. + NPAC Data and data derived from User Data is confidential information and is restricted in use as defined by the Master Services Agreement for Number Portability Administration Center/Service Management System) between iconectiv and the North American Portability Management LLC (NAPM). + https://numberportability.com/about/lrn-contacts + + + Deprecation Note: This endpoint is deprecated and will be decommissioned on Dec 1, 2025. It has been replaced with: /v2/accounts/{accountId}/phoneNumberLookup/bulk + + ## Base Path + + `https://numbers.bandwidth.com/api/v1` +servers: + - url: https://numbers.bandwidth.com/api/v1 + description: Production +paths: + /accounts/{accountId}/tnlookup: + post: + summary: Create Lookup + description: Create a Phone Number Lookup Request. + operationId: createLookup + tags: + - Phone Number Lookup + parameters: + - $ref: '#/components/parameters/accountId' + requestBody: + $ref: '#/components/requestBodies/createLookupRequest' + responses: + '202': + $ref: '#/components/responses/createLookupResponse' + '400': + $ref: '#/components/responses/tnLookupBadRequestError' + '401': + $ref: '#/components/responses/tnLookupUnauthorizedError' + '403': + $ref: '#/components/responses/tnLookupForbiddenError' + '415': + $ref: '#/components/responses/tnLookupMediaTypeError' + '429': + $ref: '#/components/responses/tnLookupTooManyRequestsError' + '500': + $ref: '#/components/responses/tnLookupInternalServerError' + /accounts/{accountId}/tnlookup/{requestId}: + get: + summary: Get Lookup Request Status + description: Get an existing Phone Number Lookup Request. + operationId: getLookupStatus + tags: + - Phone Number Lookup + parameters: + - $ref: '#/components/parameters/accountId' + - $ref: '#/components/parameters/requestId' + responses: + '200': + $ref: '#/components/responses/getLookupResponse' + '400': + $ref: '#/components/responses/tnLookupBadRequestError' + '401': + $ref: '#/components/responses/tnLookupUnauthorizedError' + '403': + $ref: '#/components/responses/tnLookupForbiddenError' + '404': + $ref: '#/components/responses/tnLookupNotFoundError' + '429': + $ref: '#/components/responses/tnLookupTooManyRequestsError' + '500': + $ref: '#/components/responses/tnLookupInternalServerError' +components: + schemas: + lookupStatusEnum: + type: string + description: >- + The status of the request (IN_PROGRESS, COMPLETE, PARTIAL_COMPLETE, or + FAILED). + enum: + - IN_PROGRESS + - COMPLETE + - PARTIAL_COMPLETE + - FAILED + example: COMPLETE + lookupRequest: + type: object + description: Create phone number lookup request. + properties: + tns: + type: array + items: + type: string + required: + - tns + createLookupResponse: + type: object + description: >- + The request has been accepted for processing but not yet finished and in + a terminal state (COMPLETE, PARTIAL_COMPLETE, or FAILED). + properties: + requestId: + type: string + description: The phone number lookup request ID from Bandwidth. + status: + $ref: '#/components/schemas/lookupStatusEnum' + lookupStatus: + type: object + description: >- + If requestId exists, the result for that request is returned. See the + Examples for details on the various responses that you can receive. + Generally, if you see a Response Code of 0 in a result for a TN, + information will be available for it. Any other Response Code will + indicate no information was available for the TN. + properties: + requestId: + type: string + description: The requestId. + example: 004223a0-8b17-41b1-bf81-20732adf5590 + status: + $ref: '#/components/schemas/lookupStatusEnum' + result: + type: array + description: The carrier information results for the specified telephone number. + items: + $ref: '#/components/schemas/lookupResult' + failedTelephoneNumbers: + type: array + description: The telephone numbers whose lookup failed. + items: + type: string + example: ['+191955512345'] + lookupResult: + type: object + description: Carrier information results for the specified telephone number. + properties: + Response Code: + type: integer + description: Our vendor's response code. + example: 0 + Message: + type: string + description: Message associated with the response code. + example: NOERROR + E.164 Format: + type: string + description: The telephone number in E.164 format. + example: '+19195551234' + Formatted: + type: string + description: The formatted version of the telephone number. + example: '(919) 555-1234' + Country: + type: string + description: The country of the telephone number. + example: US + Line Type: + type: string + description: The line type of the telephone number. + example: Mobile + Line Provider: + type: string + description: The messaging service provider of the telephone number. + example: Verizon Wireless + Mobile Country Code: + type: string + description: The first half of the Home Network Identity (HNI). + example: '310' + Mobile Network Code: + type: string + description: The second half of the HNI. + example: '010' + tnLookupRequestError: + type: object + properties: + message: + type: string + description: A description of what validation error occurred. + example: example error message + responses: + createLookupResponse: + description: Accepted + content: + application/json: + schema: + $ref: '#/components/schemas/createLookupResponse' + examples: + lookupResponseExample: + $ref: '#/components/examples/lookupInProgressExample' + getLookupResponse: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/lookupStatus' + examples: + lookupInProgressExample: + $ref: '#/components/examples/lookupInProgressExample' + lookupFailedExample: + $ref: '#/components/examples/lookupFailedExample' + lookupSingleNumberCompleteExample: + $ref: '#/components/examples/lookupSingleNumberCompleteExample' + lookupMultipleNumbersCompleteExample: + $ref: '#/components/examples/lookupMultipleNumbersCompleteExample' + lookupMultipleNumbersPartialCompleteExample: + $ref: '#/components/examples/lookupMultipleNumbersPartialCompleteExample' + lookupSingleNumberCompleteNoInfoExample: + $ref: '#/components/examples/lookupSingleNumberCompleteNoInfoExample' + tnLookupBadRequestError: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/tnLookupRequestError' + examples: + badRequest: + summary: Example Bad Request Error + value: + message: 'Some tns do not match e164 format: 1234' + tnLookupUnauthorizedError: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/tnLookupRequestError' + examples: + unauthorized: + summary: Example Unauthorized Error + value: + message: Unauthorized + tnLookupForbiddenError: + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/tnLookupRequestError' + examples: + forbidden: + summary: Example Forbidden Error + value: + message: >- + Authorization header requires 'Credential' parameter. + Authorization header requires 'Signature' parameter. + Authorization header requires 'SignedHeaders' parameter. + Authorization header requires existence of either a + 'X-Amz-Date' or a 'Date' header. Authorization=Basic + Y2tvZloPTGhHgywYIzGlcGVlcGvvcGovYTIGIt==' + tnLookupMediaTypeError: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/tnLookupRequestError' + examples: + mediaType: + summary: Example Unsupported Media Type Error + value: + message: Content-Type must be application/json. + tnLookupNotFoundError: + description: Not Found + tnLookupTooManyRequestsError: + description: Too Many Requests + content: + application/json: + schema: + $ref: '#/components/schemas/tnLookupRequestError' + examples: + mediaType: + summary: Example Too Many Requests Error + value: + message: Too many requests. + tnLookupInternalServerError: + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/tnLookupRequestError' + examples: + mediaType: + summary: Example Internal Server Error Error + value: + message: Request has not been passed further. + parameters: + accountId: + in: path + name: accountId + required: true + schema: + type: string + description: Your Bandwidth Account ID. + example: "9900000" + requestId: + name: requestId + in: path + required: true + schema: + type: string + description: The phone number lookup request ID from Bandwidth. + example: 004223a0-8b17-41b1-bf81-20732adf5590 + examples: + singleNumberRequestExample: + summary: Example Number Lookup Request for One Number + value: + tns: + - '+19195551234' + multipleNumberRequestExample: + summary: Example Number Lookup Request for Multiple Numbers + value: + tns: + - '+19195551234' + - '+19195554321' + lookupInProgressExample: + summary: Example Lookup In Progress Response + value: + requestId: 004223a0-8b17-41b1-bf81-20732adf5590 + status: IN_PROGRESS + lookupFailedExample: + summary: Example Lookup Failed Response + value: + requestId: 004223a0-8b17-41b1-bf81-20732adf5590 + status: FAILED + failedTelephoneNumbers: + - '+191955512345' + lookupSingleNumberCompleteExample: + summary: Example Single Number Lookup Complete Response + value: + requestId: 004223a0-8b17-41b1-bf81-20732adf5590 + status: COMPLETE + result: + - Response Code: 0 + Message: NOERROR + E.164 Format: '+19195551234' + Formatted: (919) 555-1234 + Country: US + Line Type: Mobile + Line Provider: Verizon Wireless + Mobile Country Code: '310' + Mobile Network Code: '010' + lookupMultipleNumbersCompleteExample: + summary: Example Multiple Numbers Lookup Complete Response + value: + requestId: 004223a0-8b17-41b1-bf81-20732adf5590 + status: COMPLETE + result: + - Response Code: 0 + Message: NOERROR + E.164 Format: '+19195551234' + Formatted: (919) 555-1234 + Country: US + Line Type: Mobile + Line Provider: Verizon Wireless + Mobile Country Code: '310' + Mobile Network Code: '010' + - Response Code: 0 + Message: NOERROR + E.164 Format: '+19195554321' + Formatted: (919) 555-4321 + Country: US + Line Type: Mobile + Line Provider: T-Mobile USA + Mobile Country Code: '310' + Mobile Network Code: '160' + lookupMultipleNumbersPartialCompleteExample: + summary: Example Multiple Numbers Lookup Partial Complete Response + value: + requestId: 004223a0-8b17-41b1-bf81-20732adf5590 + status: PARTIAL_COMPLETE + result: + - Response Code: 0 + Message: NOERROR + E.164 Format: '+19195551234' + Formatted: (919) 555-1234 + Country: US + Line Type: Mobile + Line Provider: Verizon Wireless + Mobile Country Code: '310' + Mobile Network Code: '010' + failedTelephoneNumbers: + - '+191955512345' + lookupSingleNumberCompleteNoInfoExample: + summary: Example Single Number Lookup Complete with No Information Response + value: + requestId: 004223a0-8b17-41b1-bf81-20732adf5590 + status: COMPLETE + result: + - Response Code: 3 + Message: NXDOMAIN + E.164 Format: '+19195550000' + Formatted: (919) 555-0000 + Country: US + requestBodies: + createLookupRequest: + description: Phone number lookup request. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/lookupRequest' + examples: + singleNumberRequestExample: + $ref: '#/components/examples/singleNumberRequestExample' + multipleNumberRequestExample: + $ref: '#/components/examples/multipleNumberRequestExample' + securitySchemes: + Basic: + type: http + scheme: basic + description: >- + Basic authentication is a simple authentication scheme built into the + HTTP protocol. To use it, send your HTTP requests with an Authorization + header that contains the word Basic followed by a space and a + base64-encoded string `username:password`. + + Example: `Authorization: Basic ZGVtbZpwQDU1dzByZA==` +security: + - Basic: [] +tags: + - name: Phone Number Lookup diff --git a/test/test_openapi.py b/test/test_openapi.py new file mode 100644 index 0000000..d14ee6b --- /dev/null +++ b/test/test_openapi.py @@ -0,0 +1,40 @@ +import pytest +from pytest_httpx import HTTPXMock +from test.utils import create_mock +from src.server_utils import fetch_openapi_spec + + +@pytest.mark.asyncio +async def test_fetch_openapi_spec_valid(httpx_mock: HTTPXMock): + """Test that the OpenAPI spec can be fetched and parsed correctly.""" + + create_mock(httpx_mock, "insights") + + spec = await fetch_openapi_spec("https://dev.bandwidth.com/spec/insights.yml") + + assert isinstance(spec, dict), "Fetched spec should be a dictionary" + assert "openapi" in spec, "Spec should contain 'openapi' key" + assert "info" in spec, "Spec should contain 'info' key" + assert "paths" in spec, "Spec should contain 'paths' key" + + +@pytest.mark.asyncio +async def test_fetch_openapi_spec_empty_yaml(httpx_mock: HTTPXMock): + """Test that fetching an empty spec raises an error.""" + create_mock(httpx_mock, "empty") + with pytest.raises(ValueError): + await fetch_openapi_spec("https://dev.bandwidth.com/spec/empty.yml") + + +@pytest.mark.asyncio +async def test_fetch_openapi_spec_http_error(): + """Test that fetching an invalid URL raises an HTTP error.""" + with pytest.raises(RuntimeError): + await fetch_openapi_spec("https://dev.bandwidth.com/spec/nonexistent.yml") + + +@pytest.mark.asyncio +async def test_fetch_openapi_spec_invalid_yaml(): + """Test that fetching an invalid YAML file raises a YAMLError.""" + with pytest.raises(RuntimeError): + await fetch_openapi_spec("https://example.com") diff --git a/test/test_servers.py b/test/test_servers.py index f86af8a..999b3cd 100644 --- a/test/test_servers.py +++ b/test/test_servers.py @@ -1,5 +1,7 @@ import pytest from fastmcp import FastMCP +from pytest_httpx import HTTPXMock +from test.utils import create_mock from src.servers import create_bandwidth_mcp, _create_server async def create_mcp_server(name=None, tools=None, excluded_tools=None): @@ -26,20 +28,22 @@ def calculate_expected_tools(tools, excluded_tools, total_tools=19): server_configuration_list = [ ([], []), ([], ["getReports", "createReport"]), - (["createMessage"], []), - (["generateMessagingCode", "generateVoiceCode"], []), - (["createLookup", "getLookupStatus", "createMessage"], ["listCalls"]), + (["getReports", "createReport"], []), + (["uploadMedia", "deleteMedia", "getMedia"], ["listMedia"]), (["listMedia"], ["uploadMedia", "deleteMedia", "getMedia"]), ] -@pytest.mark.skip + @pytest.mark.asyncio @pytest.mark.parametrize("tools, excluded_tools", server_configuration_list) -async def test_full_mcp_server_creation(tools, excluded_tools): +async def test_full_mcp_server_creation(tools, excluded_tools, httpx_mock: HTTPXMock): """Test that the MCP server is created correctly with included and excluded tools.""" expected_tools = calculate_expected_tools(tools, excluded_tools) name = f"Test MCP with {expected_tools} Tools" + + for name in ["messaging", "multi-factor-auth", "phone-number-lookup", "insights"]: + create_mock(httpx_mock, name) mcp = await create_mcp_server(name, tools, excluded_tools) mcp_tools = await mcp.get_tools() @@ -104,3 +108,13 @@ async def test_individual_mcp_server_creation( f"Expected base URL '{expected_base_url}', got '{server_client.base_url}'" assert server_client.headers["Authorization"] == expected_auth_header, \ f"Expected auth header '{expected_auth_header}', got '{server_client.headers['Authorization']}'" + + +@pytest.mark.asyncio +async def test_create_server_no_servers_defined(httpx_mock: HTTPXMock): + """Test that creating a server with no servers defined raises an error.""" + + create_mock(httpx_mock, "no-servers") + + with pytest.raises(ValueError, match="has no servers defined"): + await _create_server("https://dev.bandwidth.com/spec/no-servers.yml") diff --git a/test/utils.py b/test/utils.py new file mode 100644 index 0000000..e8321ca --- /dev/null +++ b/test/utils.py @@ -0,0 +1,11 @@ +from pytest_httpx import HTTPXMock + + +def create_mock(httpx_mock: HTTPXMock, spec_name: str): + """Helper function to create a mock response for HTTPX.""" + with open(f"test/fixtures/{spec_name}.yml", 'r') as f: + response_text = f.read() + httpx_mock.add_response( + url=f"https://dev.bandwidth.com/spec/{spec_name}.yml", + text=response_text + ) From d8c033de434e2cc89ea8c7a20fa5324fa8fe5981 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Wed, 20 Aug 2025 17:19:50 -0400 Subject: [PATCH 15/33] test wf --- .github/workflows/test-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-pr.yml b/.github/workflows/test-pr.yml index f1b1a6a..0f43eca 100644 --- a/.github/workflows/test-pr.yml +++ b/.github/workflows/test-pr.yml @@ -15,7 +15,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [windows-2025,ubuntu-24.04] + os: [windows-2025, ubuntu-24.04] python-version: ['3.10', '3.11', '3.12', '3.13'] fail-fast: false env: From 500e19635cc11b4146b4cc459c3fbd7b24eecfa2 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Wed, 20 Aug 2025 17:28:47 -0400 Subject: [PATCH 16/33] add black for code formatting --- dev-requirements.txt | 1 + pyproject.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index bc7860e..644d34f 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,2 +1,3 @@ pytest>=8.4.1 pytest-httpx>=0.35.0 +black>=25.1.0 diff --git a/pyproject.toml b/pyproject.toml index 4d0a852..02e7e08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ start = "app:main" [dependency-groups] dev = [ + "black>=25.1.0", "pytest>=8.4.1", "pytest-httpx>=0.35.0", ] From 91820a352ff341a16841ef1e457bc8f7ccf0051c Mon Sep 17 00:00:00 2001 From: ckoegel Date: Wed, 20 Aug 2025 17:28:54 -0400 Subject: [PATCH 17/33] format src with black --- src/app.py | 11 ++++------ src/config.py | 16 +++++++++------ src/resources.py | 1 + src/server_utils.py | 49 +++++++++++++++++++++++++++------------------ src/servers.py | 42 ++++++++++++++++---------------------- 5 files changed, 62 insertions(+), 57 deletions(-) diff --git a/src/app.py b/src/app.py index fdcf8aa..85d2fd9 100644 --- a/src/app.py +++ b/src/app.py @@ -1,16 +1,11 @@ import asyncio - from fastmcp import FastMCP - from .servers import create_bandwidth_mcp -from .config import ( - load_config, - get_enabled_tools, - get_excluded_tools -) +from .config import load_config, get_enabled_tools, get_excluded_tools mcp = FastMCP(name="Bandwidth MCP") + async def setup(mcp: FastMCP = mcp): """Setup the Bandwidth MCP server with tools and resources.""" enabled_tools = get_enabled_tools() @@ -20,10 +15,12 @@ async def setup(mcp: FastMCP = mcp): print("Setting up Bandwidth MCP server...") await create_bandwidth_mcp(mcp, enabled_tools, excluded_tools, config) + def main(): """Main function to run the Bandwidth MCP server.""" asyncio.run(setup()) mcp.run() + if __name__ == "__main__": main() diff --git a/src/config.py b/src/config.py index d9f6894..eadfa75 100644 --- a/src/config.py +++ b/src/config.py @@ -1,5 +1,4 @@ import os - from typing import Dict, List, Optional from argparse import ArgumentParser, Namespace @@ -8,8 +7,13 @@ def load_config() -> Dict[str, str]: """Load Bandwidth configuration from environment variables.""" config = {} required_vars = ["BW_USERNAME", "BW_PASSWORD"] - optional_vars = ["BW_ACCOUNT_ID", "BW_NUMBER", "BW_MESSAGING_APPLICATION_ID", "BW_VOICE_APPLICATION_ID"] - + optional_vars = [ + "BW_ACCOUNT_ID", + "BW_NUMBER", + "BW_MESSAGING_APPLICATION_ID", + "BW_VOICE_APPLICATION_ID", + ] + # Required variables for var in required_vars: if var not in os.environ: @@ -20,7 +24,7 @@ def load_config() -> Dict[str, str]: value = os.getenv(var) if value: config[var] = value - + return config @@ -53,12 +57,12 @@ def _parse_flags(cli_arg: Optional[str], env_var: str) -> Optional[List[str]]: # Try CLI argument first if cli_arg: return _parse_arg_list(cli_arg) - + # Fall back to environment variable env_value = os.getenv(env_var) if env_value: return _parse_arg_list(env_value) - + return None diff --git a/src/resources.py b/src/resources.py index 3d0710d..80be9ec 100644 --- a/src/resources.py +++ b/src/resources.py @@ -1,6 +1,7 @@ from typing import List from fastmcp.resources import HttpResource, Resource + number_order_guide_resource = HttpResource( name="Bandwidth Number Order Guide", description="Bandwidth Number Order Guide", diff --git a/src/server_utils.py b/src/server_utils.py index cde276c..d408af5 100644 --- a/src/server_utils.py +++ b/src/server_utils.py @@ -13,38 +13,46 @@ async def print_server_info(mcp: FastMCP) -> None: """Print concise server information.""" - + all_tools = await mcp.get_tools() all_resources = await mcp.get_resources() - + tool_names = list(all_tools.keys()) resource_names = [resource.name for resource in all_resources.values()] print("Bandwidth MCP Server Started") - print(f"Tools ({len(tool_names)}): {', '.join(sorted(tool_names)) if tool_names else 'None'}") - print(f"Resources ({len(resource_names)}): {', '.join(sorted(resource_names)) if resource_names else 'None'}") + print( + f"Tools ({len(tool_names)}): {', '.join(sorted(tool_names)) if tool_names else 'None'}" + ) + print( + f"Resources ({len(resource_names)}): {', '.join(sorted(resource_names)) if resource_names else 'None'}" + ) def create_route_map_fn( - enabled_tools: Optional[List[str]], - excluded_tools: Optional[List[str]] + enabled_tools: Optional[List[str]], excluded_tools: Optional[List[str]] ) -> Callable[[HTTPRoute, MCPType], MCPType]: """Create a route map function based on enabled and excluded tools. - + Args: enabled_tools: List of tools to enable. If None, all tools are enabled. excluded_tools: List of tools to exclude. Takes priority over enabled_tools. - + Returns: A function that maps routes to MCP types based on the tool configuration. """ + def route_map_fn(route: HTTPRoute, mcp_type: MCPType) -> MCPType: # Excluded tools have priority - if provided, ignore enabled tools if excluded_tools: - return mcp_type if route.operation_id not in excluded_tools else MCPType.EXCLUDE + return ( + mcp_type + if route.operation_id not in excluded_tools + else MCPType.EXCLUDE + ) if enabled_tools: return mcp_type if route.operation_id in enabled_tools else MCPType.EXCLUDE - + return mcp_type return route_map_fn @@ -58,7 +66,7 @@ def _clean_openapi_spec(spec: Dict[str, Any]) -> Dict[str, Any]: - Remove all path resources that start with 'x-' """ cleaned_spec = copy.deepcopy(spec) - + def _clean(obj: Any) -> Any: if isinstance(obj, dict): # Remove 'callbacks' and 'x-' fields @@ -67,8 +75,11 @@ def _clean(obj: Any) -> Any: del obj[k] # Remove 4xx/5xx responses if "responses" in obj: - codes_to_remove = [code for code in obj["responses"] - if str(code).startswith(("4", "5"))] + codes_to_remove = [ + code + for code in obj["responses"] + if str(code).startswith(("4", "5")) + ] for code in codes_to_remove: del obj["responses"][code] # Special handling for paths @@ -94,11 +105,11 @@ async def fetch_openapi_spec(url: str) -> Dict[str, Any]: response = await client.get(url) response.raise_for_status() spec_text = response.text - + spec_object = yaml.safe_load(spec_text) if not spec_object: raise ValueError(f"Empty or invalid YAML spec from {url}") - + return _clean_openapi_spec(spec_object) except httpx.HTTPError as e: raise RuntimeError(f"Failed to fetch OpenAPI spec from {url}: {e}") from e @@ -108,8 +119,8 @@ async def fetch_openapi_spec(url: str) -> Dict[str, Any]: def create_auth_header(username: str, password: str) -> str: """Create a basic authentication header.""" - auth_bytes = f"{username}:{password}".encode('utf-8') - return base64.b64encode(auth_bytes).decode('utf-8') + auth_bytes = f"{username}:{password}".encode("utf-8") + return base64.b64encode(auth_bytes).decode("utf-8") def add_resources(mcp: FastMCP, config: Dict[str, Any]) -> FastMCP: @@ -117,10 +128,10 @@ def add_resources(mcp: FastMCP, config: Dict[str, Any]) -> FastMCP: config_resource = FunctionResource( name="Bandwidth API Configuration", description="Object containing API credentials, application IDs, and account ID.", - tags={"bandwidth","config","credentials"}, + tags={"bandwidth", "config", "credentials"}, uri="resource://config", mime_type="application/json", - fn=lambda: config + fn=lambda: config, ) mcp.add_resource(config_resource) diff --git a/src/servers.py b/src/servers.py index 749a26a..778b303 100644 --- a/src/servers.py +++ b/src/servers.py @@ -7,38 +7,32 @@ create_route_map_fn, create_auth_header, fetch_openapi_spec, - print_server_info + print_server_info, ) api_server_info: Dict[str, Dict[str, Any]] = { - "messaging": { - "url": "https://dev.bandwidth.com/spec/messaging.yml" - }, + "messaging": {"url": "https://dev.bandwidth.com/spec/messaging.yml"}, "multi-factor-auth": { "url": "https://dev.bandwidth.com/spec/multi-factor-auth.yml" }, "phone-number-lookup": { "url": "https://dev.bandwidth.com/spec/phone-number-lookup.yml" }, - "insights": { - "url": "https://dev.bandwidth.com/spec/insights.yml" - } + "insights": {"url": "https://dev.bandwidth.com/spec/insights.yml"}, } async def _create_server( - url: str, - route_map_fn: Optional[Callable] = None, - config: Dict[str, Any] = {} + url: str, route_map_fn: Optional[Callable] = None, config: Dict[str, Any] = {} ) -> FastMCP: """Create an MCP server from the provided spec URL and credentials.""" # Fetch and clean the OpenAPI spec spec_object = await fetch_openapi_spec(url) - + # Validate spec structure if "servers" not in spec_object or not spec_object["servers"]: raise ValueError(f"OpenAPI spec from {url} has no servers defined") - + base_url = spec_object["servers"][0]["url"] auth_b64 = create_auth_header(config["BW_USERNAME"], config["BW_PASSWORD"]) @@ -46,8 +40,8 @@ async def _create_server( base_url=base_url, headers={ "Authorization": f"Basic {auth_b64}", - "User-Agent": "Bandwidth MCP Server" - } + "User-Agent": "Bandwidth MCP Server", + }, ) mcp = FastMCP.from_openapi( @@ -61,33 +55,31 @@ async def _create_server( async def create_bandwidth_mcp( - mcp: FastMCP, - enabled_tools: Optional[List[str]], + mcp: FastMCP, + enabled_tools: Optional[List[str]], excluded_tools: Optional[List[str]], - config: Dict[str, Any] = {} + config: Dict[str, Any] = {}, ) -> FastMCP: """Create the Bandwidth MCP server from all supplied APIs, taking into account enabled and excluded APIs. - + Args: mcp: The FastMCP instance to import servers into enabled_tools: List of tools to enable. If None, all tools are enabled. excluded_tools: List of tools to exclude. Takes priority over enabled_tools. config: Configuration dictionary containing API credentials and other variables. - + Returns: The FastMCP instance with all API servers imported and resources added. - + Raises: RuntimeError: If any API server fails to create or import """ route_map_fn = create_route_map_fn(enabled_tools, excluded_tools) - + for api_name, api_info in api_server_info.items(): try: server = await _create_server( - api_info["url"], - route_map_fn=route_map_fn, - config=config + api_info["url"], route_map_fn=route_map_fn, config=config ) await mcp.import_server(server) except Exception as e: @@ -95,5 +87,5 @@ async def create_bandwidth_mcp( add_resources(mcp, config) await print_server_info(mcp) - + return mcp From c4ac9730011a315cb4be8a28db0ed23cb518b2cc Mon Sep 17 00:00:00 2001 From: ckoegel Date: Wed, 20 Aug 2025 17:29:58 -0400 Subject: [PATCH 18/33] format test with black --- test/test_openapi.py | 8 +++--- test/test_servers.py | 62 +++++++++++++++++++++++++------------------- test/utils.py | 5 ++-- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/test/test_openapi.py b/test/test_openapi.py index d14ee6b..4918fdc 100644 --- a/test/test_openapi.py +++ b/test/test_openapi.py @@ -9,14 +9,14 @@ async def test_fetch_openapi_spec_valid(httpx_mock: HTTPXMock): """Test that the OpenAPI spec can be fetched and parsed correctly.""" create_mock(httpx_mock, "insights") - + spec = await fetch_openapi_spec("https://dev.bandwidth.com/spec/insights.yml") - + assert isinstance(spec, dict), "Fetched spec should be a dictionary" assert "openapi" in spec, "Spec should contain 'openapi' key" assert "info" in spec, "Spec should contain 'info' key" assert "paths" in spec, "Spec should contain 'paths' key" - + @pytest.mark.asyncio async def test_fetch_openapi_spec_empty_yaml(httpx_mock: HTTPXMock): @@ -25,7 +25,7 @@ async def test_fetch_openapi_spec_empty_yaml(httpx_mock: HTTPXMock): with pytest.raises(ValueError): await fetch_openapi_spec("https://dev.bandwidth.com/spec/empty.yml") - + @pytest.mark.asyncio async def test_fetch_openapi_spec_http_error(): """Test that fetching an invalid URL raises an HTTP error.""" diff --git a/test/test_servers.py b/test/test_servers.py index 999b3cd..1db97b0 100644 --- a/test/test_servers.py +++ b/test/test_servers.py @@ -4,15 +4,16 @@ from test.utils import create_mock from src.servers import create_bandwidth_mcp, _create_server + async def create_mcp_server(name=None, tools=None, excluded_tools=None): """Fixture to create and return a FastMCP instance.""" mcp = FastMCP(name=name or "Test MCP") config = {"BW_USERNAME": "test_user", "BW_PASSWORD": "test_pass"} enabled_tools = tools if tools is not None else [] excluded_tools = excluded_tools if excluded_tools is not None else [] - + await create_bandwidth_mcp(mcp, enabled_tools, excluded_tools, config) - + return mcp @@ -44,7 +45,7 @@ async def test_full_mcp_server_creation(tools, excluded_tools, httpx_mock: HTTPX for name in ["messaging", "multi-factor-auth", "phone-number-lookup", "insights"]: create_mock(httpx_mock, name) - + mcp = await create_mcp_server(name, tools, excluded_tools) mcp_tools = await mcp.get_tools() mcp_tool_names = list(mcp_tools.keys()) @@ -52,12 +53,16 @@ async def test_full_mcp_server_creation(tools, excluded_tools, httpx_mock: HTTPX assert isinstance(mcp, FastMCP) assert mcp.name == name, f"Expected MCP name '{name}', got '{mcp.name}'" - assert len(mcp_tools) == expected_tools, f"Expected {expected_tools} tools, got {len(mcp_tools)}" + assert ( + len(mcp_tools) == expected_tools + ), f"Expected {expected_tools} tools, got {len(mcp_tools)}" assert len(mcp_resources) == 2, f"Expected 2 resources, got {len(mcp_resources)}" if excluded_tools: for tool in excluded_tools: - assert tool not in mcp_tool_names, f"Excluded tool {tool} should not be present" + assert ( + tool not in mcp_tool_names + ), f"Excluded tool {tool} should not be present" if tools and not excluded_tools: for tool in tools: @@ -70,51 +75,56 @@ async def test_full_mcp_server_creation(tools, excluded_tools, httpx_mock: HTTPX {"BW_USERNAME": "test_user_mfa", "BW_PASSWORD": "test_pass_mfa"}, "https://mfa.bandwidth.com/api/v1/", {"generateMessagingCode", "generateVoiceCode", "verifyCode"}, - "Basic dGVzdF91c2VyX21mYTp0ZXN0X3Bhc3NfbWZh" + "Basic dGVzdF91c2VyX21mYTp0ZXN0X3Bhc3NfbWZh", ), ( "https://dev.bandwidth.com/spec/phone-number-lookup.yml", {"BW_USERNAME": "test_user_tnlookup", "BW_PASSWORD": "test_pass_tnlookup"}, "https://numbers.bandwidth.com/api/v1/", {"createLookup", "getLookupStatus"}, - "Basic dGVzdF91c2VyX3RubG9va3VwOnRlc3RfcGFzc190bmxvb2t1cA==" - ) + "Basic dGVzdF91c2VyX3RubG9va3VwOnRlc3RfcGFzc190bmxvb2t1cA==", + ), ] @pytest.mark.asyncio -@pytest.mark.parametrize("url, config, expected_base_url, expected_tools, expected_auth_header", spec_list) +@pytest.mark.parametrize( + "url, config, expected_base_url, expected_tools, expected_auth_header", spec_list +) async def test_individual_mcp_server_creation( - url, - config, - expected_base_url, - expected_tools, - expected_auth_header + url, config, expected_base_url, expected_tools, expected_auth_header ): """Test that individual MCP servers are created correctly.""" - + server = await _create_server(url, None, config) server_client = server._client - + server_tools = await server.get_tools() server_tool_names = set(server_tools.keys()) assert isinstance(server, FastMCP) - assert server.name == "Bandwidth", f"Expected server name to be 'Bandwidth', got '{server.name}'" - assert server_tool_names == expected_tools, f"Expected tools {expected_tools}, got {server_tool_names}" - assert server_client.headers["User-Agent"] == "Bandwidth MCP Server", \ - f"Expected User-Agent 'Bandwidth MCP Server', got '{server_client.headers['User-Agent']}'" - assert server_client.base_url == expected_base_url, \ - f"Expected base URL '{expected_base_url}', got '{server_client.base_url}'" - assert server_client.headers["Authorization"] == expected_auth_header, \ - f"Expected auth header '{expected_auth_header}', got '{server_client.headers['Authorization']}'" + assert ( + server.name == "Bandwidth" + ), f"Expected server name to be 'Bandwidth', got '{server.name}'" + assert ( + server_tool_names == expected_tools + ), f"Expected tools {expected_tools}, got {server_tool_names}" + assert ( + server_client.headers["User-Agent"] == "Bandwidth MCP Server" + ), f"Expected User-Agent 'Bandwidth MCP Server', got '{server_client.headers['User-Agent']}'" + assert ( + server_client.base_url == expected_base_url + ), f"Expected base URL '{expected_base_url}', got '{server_client.base_url}'" + assert ( + server_client.headers["Authorization"] == expected_auth_header + ), f"Expected auth header '{expected_auth_header}', got '{server_client.headers['Authorization']}'" @pytest.mark.asyncio async def test_create_server_no_servers_defined(httpx_mock: HTTPXMock): """Test that creating a server with no servers defined raises an error.""" - + create_mock(httpx_mock, "no-servers") - + with pytest.raises(ValueError, match="has no servers defined"): await _create_server("https://dev.bandwidth.com/spec/no-servers.yml") diff --git a/test/utils.py b/test/utils.py index e8321ca..2e10d98 100644 --- a/test/utils.py +++ b/test/utils.py @@ -3,9 +3,8 @@ def create_mock(httpx_mock: HTTPXMock, spec_name: str): """Helper function to create a mock response for HTTPX.""" - with open(f"test/fixtures/{spec_name}.yml", 'r') as f: + with open(f"test/fixtures/{spec_name}.yml", "r") as f: response_text = f.read() httpx_mock.add_response( - url=f"https://dev.bandwidth.com/spec/{spec_name}.yml", - text=response_text + url=f"https://dev.bandwidth.com/spec/{spec_name}.yml", text=response_text ) From a87d9e5a4261ad310b14ce2a1f142356b80e95fc Mon Sep 17 00:00:00 2001 From: ckoegel Date: Thu, 21 Aug 2025 17:15:05 -0400 Subject: [PATCH 19/33] add use cases guide --- README.md | 3 +++ common_use_cases.md | 54 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 common_use_cases.md diff --git a/README.md b/README.md index a7941a2..bff6b3d 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,9 @@ The `BW_MCP_TOOLS` and `BW_MCP_EXCLUDE_TOOLS` mentioned in the [Environment Vari section allow for enabling and excluding tools by name. You can also use the CLI flags `--tools` and `--exclude-tools`. Using the CLI flags will take priority over the environment variables, and providing tools to exclude will take priority over the list of enabled tools. +For a more comprehensive list of common use cases when which tools are required for each, check out our +[Common Use Cases Guide](common_use_cases.md) + ##### Tool Filtering Examples **Including only our Messaging tools** diff --git a/common_use_cases.md b/common_use_cases.md new file mode 100644 index 0000000..a6dd2f3 --- /dev/null +++ b/common_use_cases.md @@ -0,0 +1,54 @@ +# Common Use Cases + +This guide outlines some common use cases for the MCP Server, as well as the tools required for these cases. +For more information on how to include the tools mentioned in this guide, please see the +[Including or Excluding Tools](README.md#including-or-excluding-tools) section in the README. + +## Sending Text Messages + +If you're looking to send messages using the MCP server, we recommend enabling the following tools: +- `listMessages` - Get info about messages you just sent or other messages on your account. +- `createMessage` - Send SMS or MMS messages +- `createMultiChannelMessage` - Send multi-channel messages (mostly for RBM messaging) + +**Enabling these tools** +```sh +# Environment Variable +BW_MCP_TOOLS=listMessages,createMessage,createMultiChannelMessage + +# CLI Flag +--tools listMessages,createMessage,createMultiChannelMessage +``` + +## Looking up Telephone Numbers + +If you'd like to get info about a specific telephone number or list of numbers, +you'll need both our `createLookup` and `getLookupStatus` tools. +Most agents we've experimented with have been smart enough to figure out that you +need to both create a lookup request and then get its' status to actually get the TN info, +and enabling only these two tools is a good way to help your agent remember that! + +**Enabling these tools** +```sh +# Environment Variable +BW_MCP_TOOLS=createLookup,getLookupStatus + +# CLI Flag +--tools createLookup,getLookupStatus +``` + +## Utilizing our MFA Service + +To create and verify multi-factor authentication codes, you'll need our three MFA tools. +- `generateMessagingCode` - Used to generate and send an MFA code via SMS +- `generateVoiceCode` - Use to generate and send an MFA code via a phone call +- `verifyCode` - Verify an MFA code sent with one of the previous tools + +**Enabling these tools** +```sh +# Environment Variable +BW_MCP_TOOLS=generateMessagingCode,generateVoiceCode,verifyCode + +# CLI Flag +--tools generateMessagingCode,generateVoiceCode,verifyCode +``` From a9f2eb8f1bc91ca4fb278117e7681edcc6a841be Mon Sep 17 00:00:00 2001 From: ckoegel Date: Thu, 21 Aug 2025 17:18:04 -0400 Subject: [PATCH 20/33] period --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bff6b3d..7804674 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ section allow for enabling and excluding tools by name. You can also use the CLI Using the CLI flags will take priority over the environment variables, and providing tools to exclude will take priority over the list of enabled tools. For a more comprehensive list of common use cases when which tools are required for each, check out our -[Common Use Cases Guide](common_use_cases.md) +[Common Use Cases Guide](common_use_cases.md). ##### Tool Filtering Examples From 2a3eb108b835abce4b4540760da5aec8ef5e299b Mon Sep 17 00:00:00 2001 From: ckoegel Date: Fri, 22 Aug 2025 12:11:08 -0400 Subject: [PATCH 21/33] fix imports for tests --- src/app.py | 4 ++-- src/server_utils.py | 2 +- src/servers.py | 2 +- test/__init__.py | 0 test/conftest.py | 6 ++++++ test/test_openapi.py | 2 +- test/test_servers.py | 2 +- 7 files changed, 12 insertions(+), 6 deletions(-) delete mode 100644 test/__init__.py create mode 100644 test/conftest.py diff --git a/src/app.py b/src/app.py index 85d2fd9..63eb7d6 100644 --- a/src/app.py +++ b/src/app.py @@ -1,7 +1,7 @@ import asyncio from fastmcp import FastMCP -from .servers import create_bandwidth_mcp -from .config import load_config, get_enabled_tools, get_excluded_tools +from servers import create_bandwidth_mcp +from config import load_config, get_enabled_tools, get_excluded_tools mcp = FastMCP(name="Bandwidth MCP") diff --git a/src/server_utils.py b/src/server_utils.py index d408af5..2aa5fb5 100644 --- a/src/server_utils.py +++ b/src/server_utils.py @@ -8,7 +8,7 @@ from fastmcp.server.openapi import MCPType, HTTPRoute from typing import Dict, List, Optional, Any, Callable -from .resources import get_bandwidth_resources +from resources import get_bandwidth_resources async def print_server_info(mcp: FastMCP) -> None: diff --git a/src/servers.py b/src/servers.py index 778b303..2fbf330 100644 --- a/src/servers.py +++ b/src/servers.py @@ -2,7 +2,7 @@ from httpx import AsyncClient from typing import Dict, List, Optional, Callable, Any -from .server_utils import ( +from server_utils import ( add_resources, create_route_map_fn, create_auth_header, diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..f11816f --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,6 @@ +import sys +from pathlib import Path + +# Add src directory to Python path +src_path = Path(__file__).parent.parent / "src" +sys.path.insert(0, str(src_path)) diff --git a/test/test_openapi.py b/test/test_openapi.py index 4918fdc..b1fe478 100644 --- a/test/test_openapi.py +++ b/test/test_openapi.py @@ -1,6 +1,6 @@ import pytest from pytest_httpx import HTTPXMock -from test.utils import create_mock +from utils import create_mock from src.server_utils import fetch_openapi_spec diff --git a/test/test_servers.py b/test/test_servers.py index 1db97b0..4ec8969 100644 --- a/test/test_servers.py +++ b/test/test_servers.py @@ -1,7 +1,7 @@ import pytest from fastmcp import FastMCP from pytest_httpx import HTTPXMock -from test.utils import create_mock +from utils import create_mock from src.servers import create_bandwidth_mcp, _create_server From c6ce99a5b23f7665aefa2a1a7e278249b2046cbc Mon Sep 17 00:00:00 2001 From: ckoegel Date: Fri, 22 Aug 2025 15:27:00 -0400 Subject: [PATCH 22/33] add eums --- src/servers.py | 1 + test/fixtures/end-user-management.yml | 3079 +++++++++++++++++++++++++ test/test_servers.py | 4 +- 3 files changed, 3082 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/end-user-management.yml diff --git a/src/servers.py b/src/servers.py index 2fbf330..9927fe2 100644 --- a/src/servers.py +++ b/src/servers.py @@ -19,6 +19,7 @@ "url": "https://dev.bandwidth.com/spec/phone-number-lookup.yml" }, "insights": {"url": "https://dev.bandwidth.com/spec/insights.yml"}, + "end-user-management": {"url": "https://dev.bandwidth.com/spec/end-user-management.yml"} } diff --git a/test/fixtures/end-user-management.yml b/test/fixtures/end-user-management.yml new file mode 100644 index 0000000..1070049 --- /dev/null +++ b/test/fixtures/end-user-management.yml @@ -0,0 +1,3079 @@ +openapi: 3.1.0 +info: + title: End User Management + description: |- + Bandwidth's End User Management API allows you to manage addresses, + validate them, and handle compliance requirements for end users. + + - Addresses Management + - Compliance + - Requirements Packages + version: 1.0.0 + contact: + name: Bandwidth + url: https://support.bandwidth.com + email: support@bandwidth.com + termsOfService: https://www.bandwidth.com/legal/terms-of-use-bandwidthcom-web-sites/ +servers: + - url: https://api.bandwidth.com/api/v2 + description: Production + +paths: + /addresses/fields/{countryCodeA3}: + get: + summary: Get Address Fields + operationId: getAddressFields + description: Get a list of address fields that is supported as per the country and feature specific requirements. + tags: + - Addresses + parameters: + - $ref: "#/components/parameters/countryCodeA3PathParam" + responses: + "200": + $ref: "#/components/responses/getAddressFieldsResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /accounts/{accountId}/addresses/validator: + post: + summary: Validate Address + operationId: validateAddress + description: Returns features that will NOT work for the entered address. Please note that independent features/services may have additional validation and absence from excludedFeatures does not guarantee that the address will be valid for the feature/service. + tags: + - Addresses + parameters: + - $ref: "#/components/parameters/accountId" + requestBody: + $ref: "#/components/requestBodies/validateAddressRequest" + responses: + "200": + $ref: "#/components/responses/validateAddressResponse" + "400": + $ref: "#/components/responses/validateAddressBadRequestResponse" + "401": + $ref: "#/components/responses/unauthorizedError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /accounts/{accountId}/addresses: + get: + summary: List Addresses + operationId: listAddresses + description: List all addresses. The results are sorted by last updated time in reverse chronological order by default without any query filters. + tags: + - Addresses + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/addressCustomReferenceQueryParamEqStartsWithContains" + - $ref: "#/components/parameters/countryCodeA3QueryParamEq" + - $ref: "#/components/parameters/cityQueryParamEqStartsWith" + - $ref: "#/components/parameters/postalCodeQueryParamEqStartsWithContains" + - $ref: "#/components/parameters/geoValidationStatusQueryParamEq" + - $ref: "#/components/parameters/addressFieldsQueryParamContains" + - $ref: "#/components/parameters/afterCursorQueryParam" + - $ref: "#/components/parameters/limitQueryParam" + responses: + "200": + $ref: "#/components/responses/listAddressesResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + post: + summary: Create Address + operationId: createAddress + description: Create an address. Use GET /addresses/fields for full list of country specific address fields. + tags: + - Addresses + parameters: + - $ref: "#/components/parameters/accountId" + requestBody: + $ref: "#/components/requestBodies/createAddressRequest" + responses: + "201": + $ref: "#/components/responses/createAddressResponse" + "400": + $ref: "#/components/responses/createAddressBadRequestResponse" + "401": + $ref: "#/components/responses/unauthorizedError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /accounts/{accountId}/addresses/{addressId}: + get: + summary: Get Address + operationId: getAddress + description: Get an address. + tags: + - Addresses + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/addressIdPathParam" + responses: + "200": + $ref: "#/components/responses/getAddressResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + patch: + summary: Update Address + operationId: updateAddress + description: Update an address. Use GET /addresses/fields for full list of country specific address fields that can be updated. + tags: + - Addresses + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/addressIdPathParam" + requestBody: + $ref: "#/components/requestBodies/updateAddressRequest" + responses: + "200": + $ref: "#/components/responses/updateAddressResponse" + "400": + $ref: "#/components/responses/updateAddressBadRequestResponse" + "401": + $ref: "#/components/responses/unauthorizedError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /addresses/{countryCodeA3}/cityInfo: + get: + summary: List City Info + operationId: listCityInfo + description: | + List city info search results. + + Allowed search parameter combinations: + * city[startsWith]=Aalen + * postalCode[startsWith]=734 + * city[eq]=Aalen&postalCode[startsWith]=734 + tags: + - Addresses + parameters: + - $ref: "#/components/parameters/cityInfoCountryCodeA3PathParam" + - $ref: "#/components/parameters/cityInfoCityQueryParamEqStartsWith" + - $ref: "#/components/parameters/cityInfoPostalCodeQueryParamStartsWith" + responses: + "200": + $ref: "#/components/responses/listCityInfoResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /compliance/documentTypes: + get: + summary: List Document Types + operationId: listDocumentTypes + description: | + List of all accepted document types and their metadata requirements. + When adding a document, + the 'fields' properties indicated as required are mandatory. + tags: + - Compliance + responses: + "200": + $ref: "#/components/responses/listDocumentTypesResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /compliance/endUserTypes: + get: + summary: List End User Types + operationId: listEndUserTypes + description: | + List of all End user types and the accepted metadata information. + When creating an End user, + the 'fields' properties indicated as required are mandatory. + tags: + - Compliance + responses: + "200": + $ref: "#/components/responses/listEndUserTypesResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /accounts/{accountId}/compliance/requirements: + get: + summary: List End User Activation Requirements + operationId: listEndUserActivationRequirements + description: | + Lists all requirements required to validate an End user and activate services. + Requirements are set by - country, phone number type and End user type. + + The fields mentioned above can also be used in a hierarchical manner to search for requirements. + This means that the country field is mandatory when filtering based on phone number type, and similarly, + both country and phone number type fields are mandatory when filtering for End user type. + + Note: The 'fields' properties in End user requirements and accepted documents indicated as + required are mandatory to provide in the attached End user and document type assets before + submitting a requirement package. + + Explore all available fields for End user using + list End user types + and for document using list document types. + tags: + - Compliance + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/countryCodeA3QueryParamEq" + - $ref: "#/components/parameters/phoneNumberTypeQueryParamEq" + - $ref: "#/components/parameters/rpEndUserTypeQueryParamEq" + responses: + "200": + $ref: "#/components/responses/listRequirementsResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /accounts/{accountId}/compliance/documents/{documentId}: + get: + summary: Get Compliance Document Metadata + operationId: getComplianceDocumentMetadata + description: Get all the metadata details of the uploaded document. + tags: + - Compliance + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/documentIdPathParam" + responses: + "200": + $ref: "#/components/responses/getComplianceDocumentResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + patch: + summary: Update Compliance Document Metadata and Files + operationId: updateComplianceDocument + description: Modify document data and file. Updates are only allowed in `DRAFT` and `VERIFICATION_FAILED` state. + tags: + - Compliance + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/documentIdPathParam" + requestBody: + $ref: "#/components/requestBodies/updateComplianceDocumentRequest" + responses: + "200": + $ref: "#/components/responses/getComplianceDocumentResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /accounts/{accountId}/compliance/documents/{documentId}/fileContent: + get: + summary: Download Compliance Document + operationId: downloadComplianceDocuments + description: | + Download document using the document id. + Download will fail if 'fileContentExists' from get document metadata is false. + tags: + - Compliance + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/documentIdPathParam" + responses: + "200": + $ref: "#/components/responses/downloadDocumentResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /accounts/{accountId}/compliance/documents: + post: + summary: Create Compliance Document + operationId: createComplianceDocument + description: | + Upload a document with metadata as part of any compliance requirements. + One document can be used only in one requirement package. + To successfully create a document, please provide all the mandatory fields + marked in list document types. + To successfully submit a requirement package with this document, find all required fields + using list End user activation requirements. + The document can be created with minimum fields mentioned in list document types and attached to the requirement package. + After this, metadata can be updated using update document + before submitting the requirement package. + + Supported file formats: pdf, jpeg, png, doc, docx. + The maximum file size currently supported is 5 MB. + tags: + - Compliance + parameters: + - $ref: "#/components/parameters/accountId" + requestBody: + $ref: "#/components/requestBodies/addComplianceDocumentRequest" + responses: + "201": + $ref: "#/components/responses/addComplianceDocumentResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /accounts/{accountId}/compliance/endUsers: + get: + summary: List Compliance End Users + operationId: listComplianceEndUsers + description: | + List all End users of an account. + The results are sorted by last updated time in reverse chronological order by default without any query filters. + + The following filters can be used to filter the result + * End User Type + * End User Type and End User Name + * Custom Reference + + Also, the results can be sorted by following fields + * End User Name + * Custom Reference + tags: + - Compliance + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/endUserTypeQueryParamEq" + - $ref: "#/components/parameters/endUserNameQueryParam" + - $ref: "#/components/parameters/customReferenceQueryParam" + - $ref: "#/components/parameters/rpLimitQueryParam" + responses: + "200": + $ref: "#/components/responses/listEndUsersResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + post: + summary: Create Compliance End User + operationId: createComplianceEndUser + description: | + Create an End user. + This newly created End user can be attached to multiple requirement packages. + An End user resource can be created with minimum required fields. + Please find all minimum required fields for an End user type + using list End user types. + If this End user is to be attached to multiple requirement packages, + please provide all mandatory fields common to all the requirement packages where this End + user needs to be attached before submitting one of the associated requirement packages. + Find all required fields for End user for all the requirement packages where this End user needs to be + attached using list End user activation requirements. + End user metadata can be updated with extra required fields after creation + using update End user. + tags: + - Compliance + parameters: + - $ref: "#/components/parameters/accountId" + requestBody: + $ref: "#/components/requestBodies/postRequirementsPackageEndUserBody" + responses: + "201": + $ref: "#/components/responses/createEndUserResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /accounts/{accountId}/compliance/endUsers/{endUserId}: + get: + summary: Get Compliance End User + operationId: getComplianceEndUser + description: Retrieve an End User by ID. + tags: + - Compliance + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/endUserIdPathParam" + responses: + "200": + $ref: "#/components/responses/getEndUserResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + patch: + summary: Update Compliance End User + operationId: updateComplianceEndUser + description: | + Update End user's details except `type`. + Updates are only allowed in `DRAFT` and `VERIFICATION_FAILED` state. + Please note that once an End user's fields have been provided, it can be edited, but can't be cleared or removed. + tags: + - Compliance + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/endUserIdPathParam" + requestBody: + $ref: "#/components/requestBodies/patchRequirementsPackageEndUserBody" + responses: + "200": + $ref: "#/components/responses/updateEndUserResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /accounts/{accountId}/compliance/requirementsPackages: + get: + summary: List Requirements Packages + operationId: listRequirementsPackages + description: | + List all requirements packages. + The results are sorted by last updated time in reverse chronological order by default without any query filters. + + Following filters can be used to filter the result + * Country Code : List requirements packages based on the country code. + * Phone Number Type : List requirements packages based on the phone number type. + * End user Type : List requirements packages based on the End user type. + * Custom Reference : List requirements packages based on the custom reference. + * Status : List requirements packages based on the status. + + tags: + - Requirements Packages + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/rpCountryCodeA3QueryParamEq" + - $ref: "#/components/parameters/rpPhoneNumberTypeQueryParamEq" + - $ref: "#/components/parameters/rpEndUserTypeQueryParamEq" + - $ref: "#/components/parameters/statusQueryParamEq" + - $ref: "#/components/parameters/rpCustomReferenceQueryParamStartsWith" + - $ref: "#/components/parameters/rpLimitQueryParam" + responses: + "200": + $ref: "#/components/responses/listRequirementsPackageResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + post: + summary: Create Requirements Package + operationId: createRequirementsPackage + description: | + Create a requirements package with a set of requirements. + Note: Country, Phone Number Type and End User Type cannot be changed once created. + tags: + - Requirements Packages + parameters: + - $ref: "#/components/parameters/accountId" + requestBody: + $ref: "#/components/requestBodies/postRequirementsPackageBody" + responses: + "201": + $ref: "#/components/responses/createRequirementsPackageResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /accounts/{accountId}/compliance/requirementsPackages/{requirementsPackageId}: + get: + summary: Get Requirements Package + operationId: getRequirementsPackage + description: Retrieve a requirements package using the id. + tags: + - Requirements Packages + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/requirementsPackageIdPathParam" + responses: + "200": + $ref: "#/components/responses/getRequirementsPackageResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + patch: + summary: Update Requirements Package + operationId: patchRequirementsPackage + description: | + Update Requirements package status to `SUBMITTED` to submit a package. + * 'acknowledgements' is required for submitting the package with 'allDetailsAccurate' set to true. + * Once submitted, all associated assets will be locked and cannot be modified. + Update custom reference, email, or callback. + tags: + - Requirements Packages + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/requirementsPackageIdPathParam" + requestBody: + $ref: "#/components/requestBodies/patchRequirementsPackageBody" + responses: + "200": + $ref: "#/components/responses/updateRequirementsPackageResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /accounts/{accountId}/compliance/requirementsPackages/{requirementsPackageId}/assets: + get: + summary: Get Requirements Package Assets + operationId: getRequirementsPackageAssets + description: Retrieve all assets attached to the requirements package with their details + tags: + - Requirements Packages + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/requirementsPackageIdPathParam" + responses: + "200": + $ref: "#/components/responses/getRequirementsPackageAssetsResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + post: + summary: Attach Requirements Package Asset + operationId: attachRequirementsPackageAsset + description: | + Attach an asset to the requirements package. + tags: + - Requirements Packages + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/requirementsPackageIdPathParam" + requestBody: + $ref: "#/components/requestBodies/postRequirementsPackageAssetsBody" + responses: + "201": + $ref: "#/components/responses/createRequirementsPackageAssetResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /accounts/{accountId}/compliance/requirementsPackages/{requirementsPackageId}/assets/{assetId}: + delete: + summary: Detach Requirements Package Asset + operationId: detachRequirementsPackageAsset + description: Detach an asset from the requirements package. + tags: + - Requirements Packages + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/requirementsPackageIdPathParam" + - $ref: '#/components/parameters/requirementsPackageAssetIdPathParam' + responses: + "204": + description: No Content + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /accounts/{accountId}/compliance/numberActivation/validator: + post: + summary: Validate Number Activation + operationId: validateNumberActivation + description: Validate number activation requirements. + tags: + - Requirements Packages + parameters: + - $ref: "#/components/parameters/accountId" + requestBody: + $ref: "#/components/requestBodies/postNumberActivationValidatorBody" + responses: + "200": + $ref: "#/components/responses/validateNumberActivationResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" + /accounts/{accountId}/compliance/requirementsPackages/{requirementsPackageId}/history: + get: + tags: + - Requirements Packages + summary: Get Requirements Package History + operationId: getRequirementsPackageHistory + description: Get the history of a requirements package. + parameters: + - $ref: "#/components/parameters/accountId" + - $ref: "#/components/parameters/requirementsPackageIdPathParam" + responses: + "200": + $ref: "#/components/responses/getRequirementsPackageHistoryResponse" + "400": + $ref: "#/components/responses/badRequestError" + "401": + $ref: "#/components/responses/unauthorizedEUMSError" + "403": + $ref: "#/components/responses/forbiddenError" + "404": + $ref: "#/components/responses/notFoundError" + "405": + $ref: "#/components/responses/methodNotAllowedError" + "429": + $ref: "#/components/responses/tooManyRequestsError" + "500": + $ref: "#/components/responses/internalServerError" +components: + parameters: + accountId: + in: path + name: accountId + required: true + schema: + type: string + description: Your Bandwidth Account ID. + example: "9900000" + addressIdPathParam: + in: path + name: addressId + required: true + schema: + type: string + example: daa9dd0f-de97-4103-8530-b31bf4be8fc0 + description: The Address ID. + cityInfoCountryCodeA3PathParam: + name: countryCodeA3 + description: Country Code A3. + in: path + required: true + schema: + type: string + example: DEU + countryCodeA3PathParam: + name: countryCodeA3 + description: Country Code A3 + in: path + required: true + schema: + type: string + example: FRA + cityInfoCityQueryParamEqStartsWith: + in: query + name: city + required: false + style: deepObject + explode: true + schema: + $ref: "#/components/schemas/queryParamStringEqStartsWith" + examples: + eq: + value: + eq: Aalen + startsWith: + value: + startsWith: Aal + description: The name of the city. The system defaults to return all cities + when the query parameter is not passed. The countryCodeA3 is a required parameter + with city. Some phone number types like TOLL_FREE may not have any associated + cities. + cityInfoPostalCodeQueryParamStartsWith: + in: query + name: postalCode + required: false + style: deepObject + explode: true + schema: + type: object + properties: + startsWith: + type: string + example: "734" + description: | + The postal code of the address. The system defaults to return all + postal codes when the query parameter is not passed. The countryCodeA3 is + a required parameter with postalCode. + addressCustomReferenceQueryParamEqStartsWithContains: + in: query + name: customReference + required: false + style: deepObject + explode: true + schema: + $ref: "#/components/schemas/queryParamStringEqStartsWithContains" + examples: + eq: + value: + eq: home_office + startsWith: + value: + startsWith: home + contains: + value: + contains: office + description: | + Custom reference enables flexible search operations including exact matches (eq), + prefix-based searches (startsWith), and substring-based searches (contains). + geoValidationStatusQueryParamEq: + in: query + name: geoValidationStatus + required: false + style: deepObject + explode: true + schema: + type: object + properties: + eq: + $ref: "#/components/schemas/geoValidationStatusEnum" + example: + eq: GEO_VALID + description: | + Geo validation status which can be one of: + GEO_VALID, NOT_GEO_VALID, or NOT_GEO_VALIDATED. + countryCodeA3QueryParamEq: + in: query + name: countryCodeA3 + required: false + style: deepObject + explode: true + schema: + $ref: "#/components/schemas/queryParamStringEq" + example: + eq: USA + description: Country code of the address in ISO 3166-1 alpha-3 format. + cityQueryParamEqStartsWith: + in: query + name: city + required: false + style: deepObject + explode: true + schema: + $ref: "#/components/schemas/queryParamStringEqStartsWithContains" + examples: + eq: + value: + eq: Seattle + startsWith: + value: + startsWith: Seat + contains: + value: + contains: eattl + description: The name of the city. The system defaults to return all cities + when the query parameter is not passed. The countryCodeA3 is a required parameter + with city. Some phone number types like TOLL_FREE may not have any associated + cities. + postalCodeQueryParamEqStartsWithContains: + in: query + name: postalCode + required: false + style: deepObject + explode: true + schema: + $ref: "#/components/schemas/queryParamStringEqStartsWithContains" + examples: + eq: + value: + eq: "98072" + startsWith: + value: + startsWith: "9807" + contains: + value: + contains: "807" + description: The postal code of the address. The system defaults to return all + postal codes when the query parameter is not passed. The countryCodeA3 is + a required parameter with postalCode. + addressFieldsQueryParamContains: + description: Any of the location-related fields in the address can be used. + examples: + contains: + value: + contains: 123 Main + explode: true + in: query + name: addressFields + required: false + schema: + $ref: '#/components/schemas/queryParamStringContains' + afterCursorQueryParam: + in: query + name: afterCursor + required: false + schema: + type: string + example: ArAnD0m1d + description: Returns the page after the last record on the current page. + limitQueryParam: + in: query + name: limit + required: false + schema: + type: integer + default: 10 + minimum: 1 + maximum: 100 + example: 50 + description: Limit the number of returned records. + endUserIdPathParam: + name: endUserId + description: EndUser ID + in: path + required: true + schema: + type: string + example: "878345" + endUserTypeQueryParamEq: + in: query + name: type + required: false + style: deepObject + explode: true + schema: + type: object + properties: + eq: + $ref: "#/components/schemas/endUserTypeEnum" + example: + eq: BUSINESS + description: The type of end user. + rpEndUserTypeQueryParamEq: + in: query + name: endUserType + required: false + style: deepObject + explode: true + schema: + type: object + properties: + eq: + $ref: "#/components/schemas/endUserTypeEnum" + example: + eq: RESIDENTIAL + description: The type of end user. + endUserNameQueryParam: + in: query + name: endUserName + required: false + style: deepObject + explode: true + schema: + type: object + allOf: + - $ref: "#/components/schemas/queryParamStringStartsWith" + - type: object + properties: + sort: + $ref: "#/components/schemas/sortEnum" + examples: + RESIDENTIAL: + value: + startsWith: Adam + BUSINESS: + value: + startsWith: Alphabet + description: The End user name in order 'FirstName LastName' or 'Business Name' + rpLimitQueryParam: + in: query + name: limit + required: false + schema: + type: integer + default: 10 + minimum: 1 + maximum: 100 + example: 50 + description: Limit the number of returned records. + customReferenceQueryParam: + in: query + name: customReference + required: false + style: deepObject + explode: true + schema: + type: object + allOf: + - $ref: "#/components/schemas/queryParamStringStartsWith" + - type: object + properties: + sort: + $ref: "#/components/schemas/sortEnum" + example: + startsWith: home + description: Custom reference filter, and can be combined with 'customReference[sort]' + documentIdPathParam: + in: path + name: documentId + required: true + schema: + type: string + example: ArAnD0m1d + description: Document ID. + phoneNumberTypeQueryParamEq: + in: query + name: phoneNumberType + required: true + style: deepObject + explode: true + schema: + type: object + properties: + eq: + $ref: "#/components/schemas/phoneNumberTypeEnum" + example: + eq: GEOGRAPHIC + description: The type of phone number. + requirementsPackageAssetIdPathParam: + name: assetId + description: Requirements package asset Id + in: path + required: true + schema: + type: string + example: 925e38bb-6d09-4b25-9828-afcbe9417e53 + statusQueryParamEq: + in: query + name: status + description: The status of the item + required: false + style: deepObject + explode: true + schema: + $ref: "#/components/schemas/queryParamStringEq" + example: + eq: VERIFIED + rpCustomReferenceQueryParamStartsWith: + in: query + name: customReference + description: Filter by customReference(startsWith). + required: false + style: deepObject + explode: true + schema: + type: object + properties: + startsWith: + minLength: 1 + maxLength: 100 + example: + startsWith: home + rpCountryCodeA3QueryParamEq: + in: query + name: countryCodeA3 + description: Country code of the address in ISO 3166-1 alpha-3 format. + required: false + style: deepObject + explode: true + schema: + $ref: "#/components/schemas/queryParamStringEq" + example: + eq: USA + rpPhoneNumberTypeQueryParamEq: + in: query + name: phoneNumberType + description: The type of phone number. + required: false + style: deepObject + explode: true + schema: + type: object + properties: + eq: + $ref: "#/components/schemas/phoneNumberTypeEnum" + example: + eq: GEOGRAPHIC + requirementsPackageIdPathParam: + name: requirementsPackageId + description: Requirements Package ID + in: path + required: true + schema: + type: string + example: 815e38bb-6d09-4b25-9828-afcbe9417e53 + requestBodies: + createAddressRequest: + content: + application/json: + schema: + $ref: "#/components/schemas/createAddressRequestData" + updateAddressRequest: + content: + application/json: + schema: + $ref: "#/components/schemas/updateAddressData" + validateAddressRequest: + content: + application/json: + schema: + $ref: "#/components/schemas/validateAddressRequestData" + patchRequirementsPackageEndUserBody: + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/endUserData" + properties: + type: + readOnly: true + examples: + patchEndUserBodyBasicExample: + $ref: "#/components/examples/patchEndUserBodyBasicExample" + postRequirementsPackageEndUserBody: + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/endUserData" + required: + - type + addComplianceDocumentRequest: + description: Add a document with any metadata + content: + multipart/form-data: + schema: + type: object + properties: + metadata: + $ref: "#/components/schemas/complianceDocumentData" + file: + type: string + format: binary + encoding: + file: + contentType: application/pdf, image/jpeg, image/jpg, image/png, application/msword, + application/vnd.openxmlformats-officedocument.wordprocessingml.document + style: deepObject + explode: true + metadata: + contentType: application/json + updateComplianceDocumentRequest: + description: Update document data along with any new file change. Type cannot + be modified + content: + multipart/form-data: + schema: + type: object + properties: + metadata: + $ref: "#/components/schemas/complianceDocumentUpdateData" + file: + type: string + format: binary + postNumberActivationValidatorBody: + content: + application/json: + schema: + $ref: "#/components/schemas/numberActivationRequirementsSet" + postRequirementsPackageAssetsBody: + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/assetData" + required: + - assetReferenceId + - assetType + examples: + postRequirementsPackageAssetBodyBasicExample: + $ref: "#/components/examples/postRequirementsPackageAssetBodyBasicExample" + patchRequirementsPackageBody: + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/requirementsPackageData" + - type: object + properties: + countryCodeA3: + readOnly: true + phoneNumberType: + readOnly: true + endUserType: + readOnly: true + status: + description: Status change to requirement package. Only allowed value is SUBMITTED + type: string + example: SUBMITTED + example: + customReference: home_office + email: foo@bar.com + status: SUBMITTED + postRequirementsPackageBody: + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/requirementsPackageData" + required: + - countryCodeA3 + - phoneNumberType + - endUserType + responses: + createAddressResponse: + description: Created + content: + application/json: + schema: + $ref: "#/components/schemas/createUpdateAddressResponse" + listCityInfoResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/listCityInfoResponse" + updateAddressResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/createUpdateAddressResponse" + getAddressResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/genericAddressResponse" + listAddressesResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/listAddressesResponse" + validateAddressResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/validateAddressResponse" + getAddressFieldsResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/getAddressFieldsResponse" + createAddressBadRequestResponse: + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/addressBadRequest" + examples: + badRequestAddressSuggestionExample: + $ref: "#/components/examples/geoValidationAddressSuggestionExample" + badRequestAddressMissingGeoValidationFeatureExample: + $ref: "#/components/examples/geoValidationDisabledExample" + validateAddressBadRequestResponse: + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/genericError" + examples: + badRequestErrorExample: + $ref: "#/components/examples/geoValidationAddressSuggestionExample" + badRequestError: + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/genericError" + examples: + badRequestErrorExample: + $ref: "#/components/examples/badRequestErrorExample" + notFoundError: + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/genericError" + examples: + notFoundErrorExample: + $ref: "#/components/examples/notFoundErrorExample" + methodNotAllowedError: + description: Method Not Allowed + content: + application/json: + schema: + $ref: "#/components/schemas/genericError" + examples: + methodNotAllowedErrorExample: + $ref: "#/components/examples/methodNotAllowedErrorExample" + tooManyRequestsError: + description: Too Many Requests + content: + application/json: + schema: + $ref: "#/components/schemas/genericError" + examples: + tooManyRequestsErrorExample: + $ref: "#/components/examples/tooManyRequestsErrorExample" + internalServerError: + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/genericError" + examples: + internalServerErrorExample: + $ref: "#/components/examples/internalServerErrorExample" + forbiddenError: + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/genericError" + examples: + forbiddenErrorExample: + $ref: "#/components/examples/forbiddenErrorExample" + unauthorizedError: + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/genericError" + examples: + unauthorizedErrorExample: + $ref: "#/components/examples/unauthorizedErrorExample" + updateAddressBadRequestResponse: + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/updateGeoValidationStatusSuggestionAddressResponse" + updateEndUserResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/endUserResponse" + getEndUserResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/endUserResponse" + createEndUserResponse: + description: Created + content: + application/json: + schema: + $ref: "#/components/schemas/endUserResponse" + listEndUsersResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/endUserListResponse" + addComplianceDocumentResponse: + description: Created + content: + application/json: + schema: + $ref: "#/components/schemas/createUpdateComplianceDocumentResponse" + downloadDocumentResponse: + description: OK + content: + application/octet-stream: + schema: + type: string + format: binary + listRequirementsResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/requirementsListResponse" + getComplianceDocumentResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/getComplianceDocumentResponse" + listEndUserTypesResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/endUserTypesListResponse" + listDocumentTypesResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/documentTypesListResponse" + unauthorizedEUMSError: + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/unauthorizedComplianceResponse" + validateNumberActivationResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/numberActivationValidatorResponse" + getRequirementsPackageHistoryResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/requirementsPackageHistoryResponse" + createRequirementsPackageAssetResponse: + description: Created + content: + application/json: + schema: + $ref: "#/components/schemas/requirementsPackageAssetsPostResponse" + listRequirementsPackageResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/requirementsPackageListResponse" + getRequirementsPackageAssetsResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/requirementsPackageAssetsGetResponse" + updateRequirementsPackageResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/requirementsPackageCreateUpdateResponse" + examples: + requirementsPackageResponseExample: + $ref: "#/components/examples/requirementsPackageResponseExample" + createRequirementsPackageResponse: + description: Created + content: + application/json: + schema: + $ref: "#/components/schemas/requirementsPackageCreateUpdateResponse" + examples: + requirementsPackageResponseExample: + $ref: "#/components/examples/requirementsPackageResponseExample" + getRequirementsPackageResponse: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/requirementsPackageGetResponse" + examples: + requirementsPackageResponseExample: + $ref: "#/components/examples/requirementsPackageResponseExample" + schemas: + sortEnum: + type: string + enum: + - ASC + - DESC + geoValidationStatusEnum: + type: string + enum: + - GEO_VALID + - NOT_GEO_VALID + - NOT_GEO_VALIDATED + description: | + The geo validation status of the address. + - `GEO_VALID`: The address is valid and geo validated. + - `NOT_GEO_VALID`: The address is not valid or geo validated. + - `NOT_GEO_VALIDATED`: The address has not been geo validated yet. + endUserTypeEnum: + type: string + description: The type of end user + enum: + - BUSINESS + - RESIDENTIAL + - SOLE_PROPRIETOR + example: BUSINESS + phoneNumberTypeEnum: + type: string + description: The type of phone number. + enum: + - GEOGRAPHIC + - NATIONAL + - MOBILE + - TOLL_FREE + - SHARED_COST + example: GEOGRAPHIC + createAddressRequestData: + allOf: + - $ref: "#/components/schemas/validateAddressRequestData" + - type: object + properties: + geoValidation: + $ref: "#/components/schemas/geoValidation" + addressRequestData: + type: object + properties: + customReference: + $ref: "#/components/schemas/customReference" + countryCodeA3: + $ref: "#/components/schemas/countryCodeA3" + addressLine1: + $ref: "#/components/schemas/addressLine1" + addressLine2: + $ref: "#/components/schemas/addressLine2" + city: + $ref: "#/components/schemas/addressCity" + stateAbbreviation: + $ref: "#/components/schemas/stateAbbreviation" + postalCode: + $ref: "#/components/schemas/postalCode" + addressId: + type: string + description: Address ID + example: daa9dd0f-de97-4103-8530-b31bf4be8fc0 + customReference: + type: string + minLength: 1 + maxLength: 100 + description: | + A custom reference name. + It can be used for your own reference to the item to easily identify one of your customers, + requirements package, enduser etc. + example: home_office + countryCodeA3: + type: string + description: Country code of the address in ISO 3166-1 alpha-3 format. + example: USA + addressLine1: + type: string + description: The first line of the address. + example: 123 Main St + addressLine2: + type: string + description: The second line of the address. + example: Suite 200 + stateAbbreviation: + type: string + description: The state abbreviation of the address. + example: WA + geoValidation: + type: object + properties: + required: + type: boolean + description: | + Flag to indicate if the address should be geo validated or + not. By default the value is set to true. + example: true + createOnFailure: + type: boolean + description: | + Flag to indicate if the address should be created even if geo + validation fails. 'false' indicates that the creation request will fail + and “suggestedAddresses” will be provided in the response. By default + the value is set to false. + example: false + unsupportedFeatures: + description: Excluded Features + type: array + items: + type: object + properties: + featureName: + $ref: "#/components/schemas/featureNameEnum" + addressCriteria: + type: array + items: + type: object + properties: + description: + type: string + description: | + A human-readable explanation that SHOULD be specific + to this occurrence of the problem + example: | + Field 'province' is optional, but is required for the feature 'REQUIREMENTS_PACKAGE' + featureNameEnum: + type: string + description: A unique identifier for feature. + enum: + - REQUIREMENTS_PACKAGE + - EMERGENCY + - PORTING + addressFieldSchema: + type: object + properties: + fieldName: + type: string + description: The address field to be provided. + example: postalCode + friendlyName: + type: string + description: The friendly name of the address field. + example: Postal code + description: + type: string + description: The description of the address field. + example: | + Enter the 5-digit ZIP code for your location. You can also enter + a 9-digit ZIP+4 code. For example, 27607 or 27607-6789 are valid ZIP codes. + type: + $ref: "#/components/schemas/fieldType" + maxLength: + $ref: "#/components/schemas/fieldMaxLength" + required: + type: boolean + description: The address field is required or optional + example: true + changeable: + type: boolean + description: The address field value can be updated or not + example: false + usedBy: + type: array + items: + type: object + properties: + feature: + type: string + description: Feature name + example: REQUIREMENTS_PACKAGE + required: + type: boolean + description: The address field is required or optional for the feature + example: true + validateAddressRequestData: + type: object + allOf: + - $ref: "#/components/schemas/addressRequestData" + - type: object + properties: + geoValidation: + $ref: "#/components/schemas/geoValidation" + updateAddressData: + type: object + properties: + customReference: + $ref: "#/components/schemas/customReference" + geoValidation: + $ref: "#/components/schemas/geoValidation" + validateAddressResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + type: object + properties: + address: + $ref: "#/components/schemas/validateAddressResponseData" + geoValidationStatus: + $ref: "#/components/schemas/geoValidationStatusEnum" + excludedFeatures: + $ref: "#/components/schemas/unsupportedFeatures" + errors: + type: array + items: + $ref: "#/components/schemas/error" + getAddressFieldsResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + type: object + properties: + addressFields: + type: array + items: + $ref: "#/components/schemas/addressFieldSchema" + errors: + type: array + items: + $ref: "#/components/schemas/error" + addressCity: + type: string + description: The city of the address. + example: Seattle + fieldType: + type: string + description: The data type of the field + example: string + fieldMaxLength: + type: integer + description: The length of the field + example: 256 + postalCode: + type: string + description: The postal code of the address. + example: "98104" + createdDateTime: + type: string + description: The date and time this object was created in ISO 8601 format + example: "2024-03-11T04:09:25.399Z" + updatedDateTime: + type: string + description: The date and time this object was last updated in ISO 8601 format + example: "2024-03-11T04:09:25.399Z" + errorCode: + type: integer + format: int32 + description: | + An application-specific error code for services with extensive + error scenarios to supplement `description` + minimum: 4 + example: 51130 + errorDescription: + type: string + description: | + A human-readable explanation that SHOULD be specific to this occurrence + of the problem + example: There was an issue with a field in your request body + errorType: + type: string + description: | + A short, human-readable summary of the problem that SHOULD NOT + change from occurrence to occurrence of the problem + example: REQUEST_ERROR + errorSource: + type: object + properties: + parameter: + type: string + description: A string indicating which URI query parameter caused the error + example: someParameter + field: + type: string + description: A string indicating which request body field caused the error + example: someField + header: + type: string + description: A string indicating which header field caused the error + example: someHeader + reference: + type: string + description: | + A string that references a resource ID or path to the resource + (or non-existent resource) causing the error + example: /some/reference + queryParamStringEqStartsWith: + type: object + properties: + eq: + type: string + startsWith: + type: string + queryParamStringContains: + type: object + properties: + contains: + type: string + queryParamStringEqStartsWithContains: + type: object + properties: + contains: + type: string + eq: + type: string + startsWith: + type: string + queryParamStringEq: + type: object + properties: + eq: + type: string + genericError: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + type: object + nullable: true + errors: + type: array + items: + $ref: "#/components/schemas/error" + required: + - links + - data + - errors + updateGeoValidationStatusSuggestionAddressResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + type: object + properties: + address: + $ref: "#/components/schemas/validateAddressResponseData" + errors: + type: array + items: + $ref: "#/components/schemas/error" + example: + - code: 205617 + description: | + Address provided is not geo-valid. + Please refer suggestedAddresses for a list of geo valid addresses + meta: + suggestedAddresses: + - city: "SEATTLE" + countryCodeA3: USA + stateAbbreviation: WA + postalCode: "98104" + addressLine1: 123 S MAIN ST STE 200 + createUpdateAddressResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + type: object + properties: + address: + $ref: "#/components/schemas/addressResponseData" + errors: + type: array + items: + $ref: "#/components/schemas/error" + listAddressesResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + type: array + items: + $ref: "#/components/schemas/listAddressesResponseData" + errors: + type: array + items: + $ref: "#/components/schemas/error" + genericAddressResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + type: object + properties: + address: + $ref: "#/components/schemas/addressResponseData" + errors: + type: array + items: + $ref: "#/components/schemas/error" + listCityInfoResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + type: array + items: + $ref: "#/components/schemas/cityInfoData" + errors: + type: array + items: + $ref: "#/components/schemas/error" + addressResponseData: + allOf: + - $ref: "#/components/schemas/validateAddressResponseData" + - type: object + properties: + geoValidationStatus: + $ref: "#/components/schemas/geoValidationStatusEnum" + addressId: + $ref: "#/components/schemas/addressId" + createdDateTime: + $ref: "#/components/schemas/createdDateTime" + updatedDateTime: + $ref: "#/components/schemas/updatedDateTime" + listAddressesResponseData: + type: object + properties: + address: + $ref: "#/components/schemas/addressResponseData" + validateAddressResponseData: + type: object + properties: + customReference: + $ref: "#/components/schemas/customReference" + countryCodeA3: + $ref: "#/components/schemas/countryCodeA3" + addressLine1: + $ref: "#/components/schemas/addressLine1" + addressLine2: + $ref: "#/components/schemas/addressLine2" + city: + $ref: "#/components/schemas/addressCity" + stateAbbreviation: + $ref: "#/components/schemas/stateAbbreviation" + postalCode: + $ref: "#/components/schemas/postalCode" + cityInfoData: + type: array + items: + type: object + properties: + city: + type: string + description: The name of the city. + example: Seattle + postalCode: + type: string + description: The postal code of the address. + example: "98104" + link: + description: The URI link details to self or a related resource with the relation + type: object + properties: + href: + type: string + description: URI of the link + example: /resource/uri + rel: + type: string + description: Specifies the relationship between this link and the resource + example: self + method: + type: string + description: HTTP method to be used + example: GET + required: + - href + - rel + - method + error: + type: object + properties: + id: + type: string + description: A unique identifier for this particular instance of this problem + example: optional-error-id + type: + $ref: "#/components/schemas/errorType" + description: + $ref: "#/components/schemas/errorDescription" + code: + $ref: "#/components/schemas/errorCode" + meta: + type: object + description: A meta object containing application-specific information or + non-standard metadata about the error + source: + $ref: "#/components/schemas/errorSource" + required: + - code + - description + addressBadRequest: + description: addressBadRequest + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + type: object + nullable: true + errors: + type: array + items: + $ref: "#/components/schemas/error" + complianceDocumentUpdateData: + description: Metadata of updated document + type: object + properties: + customReference: + $ref: "#/components/schemas/customReference" + description: + $ref: "#/components/schemas/documentDescription" + fields: + $ref: "#/components/schemas/fields" + endUserResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + $ref: "#/components/schemas/endUserData" + errors: + $ref: "#/components/schemas/errors" + endUserListResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + type: array + items: + $ref: "#/components/schemas/endUserData" + errors: + $ref: "#/components/schemas/errors" + createUpdateComplianceDocumentResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + $ref: "#/components/schemas/complianceDocumentResponseData" + errors: + $ref: "#/components/schemas/errors" + requirementsListResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + type: array + items: + $ref: "#/components/schemas/requirementData" + errors: + $ref: "#/components/schemas/errors" + getComplianceDocumentResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + $ref: "#/components/schemas/complianceDocumentResponseData" + errors: + $ref: "#/components/schemas/errors" + endUserTypesListResponse: + description: Response with all End user type details + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + type: array + items: + $ref: "#/components/schemas/endUserTypeData" + errors: + $ref: "#/components/schemas/errors" + documentTypesListResponse: + description: Response with all document type details + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + type: array + items: + $ref: "#/components/schemas/documentTypeData" + errors: + $ref: "#/components/schemas/errors" + complianceDocumentResponseData: + allOf: + - $ref: "#/components/schemas/complianceDocumentData" + - type: object + properties: + documentId: + $ref: "#/components/schemas/documentId" + status: + $ref: "#/components/schemas/documentStatusEnum" + fileContentExists: + $ref: "#/components/schemas/fileContentExists" + createdDateTime: + $ref: "#/components/schemas/createdDateTime" + updatedDateTime: + $ref: "#/components/schemas/updatedDateTime" + fileName: + $ref: "#/components/schemas/fileName" + complianceDocumentData: + type: object + properties: + customReference: + $ref: "#/components/schemas/customReference" + type: + $ref: "#/components/schemas/complianceDocumentType" + description: + $ref: "#/components/schemas/complianceDocumentDescription" + fields: + $ref: "#/components/schemas/fields" + fileContentExists: + type: boolean + description: Indicates if file exists on system + example: false + documentId: + type: string + description: Unique identifier of the document + format: uuid + example: daa9dd0f-de97-4103-8530-b31bf4be8fc0 + documentStatusEnum: + type: string + description: Document Statuses + enum: + - DRAFT + - SUBMITTED + - PROCESSING + - VERIFIED + - VERIFICATION_FAILED + example: SUBMITTED + complianceDocumentType: + type: string + description: Unique name representing the type of document + example: tradeLicense + documentDescription: + type: string + description: Description about the document + example: Letter of Authorization + complianceDocumentDescription: + type: string + description: Description about the document + maxLength: 1000 + example: Trade License + fileName: + type: string + queryParamStringStartsWith: + type: object + properties: + startsWith: + type: string + unauthorizedComplianceResponse: + type: object + properties: + timestamp: + type: integer + format: int64 + description: Epoch timestamp of the error occurrence. + example: 1742186541633 + status: + type: integer + example: 401 + description: HTTP status code. + error: + type: string + description: Error message. + example: Unauthorized + path: + type: string + description: The request path that caused the error. + example: /api/v2/self-resource + required: + - timestamp + - status + - error + - path + requirementData: + type: object + properties: + countryCodeA3: + $ref: "#/components/schemas/countryCodeA3String" + phoneNumberType: + $ref: "#/components/schemas/phoneNumberTypeEnum" + endUserType: + $ref: "#/components/schemas/endUserTypeEnum" + requirement: + $ref: "#/components/schemas/requirement" + endUserTypeData: + description: EndUser type specification + type: object + properties: + type: + $ref: "#/components/schemas/endUserTypeEnum" + fields: + description: List of all accepted End user fields + type: array + items: + $ref: "#/components/schemas/endUserField" + example: + - fieldName: salutation + friendlyName: Salutation + type: String + values: + - MR + - MRS + required: true + - fieldName: firstName + friendlyName: First Name + type: String + maxLength: 300 + required: true + - friendlyName: Tax ID Number + fieldName: taxIdNumber + type: String + maxLength: 300 + required: false + endUserData: + type: object + properties: + type: + $ref: "#/components/schemas/endUserTypeEnum" + customReference: + $ref: "#/components/schemas/customReference" + fields: + $ref: "#/components/schemas/fields" + endUserId: + $ref: "#/components/schemas/endUserId" + status: + $ref: "#/components/schemas/endUserStatusEnum" + createdDateTime: + $ref: "#/components/schemas/createdDateTime" + updatedDateTime: + $ref: "#/components/schemas/updatedDateTime" + endUserId: + type: string + description: Unique identifier of the endUser + format: uuid + readOnly: true + example: dcb9dd0f-de97-4103-8530-b31bf4be8fc0 + endUserStatusEnum: + type: string + description: End user status + enum: + - DRAFT + - SUBMITTED + - VERIFIED + - VERIFICATION_FAILED + example: DRAFT + countryCodeA3String: + type: string + description: Country code in ISO 3166-1 alpha-3 format + example: FRA + errors: + type: array + items: + $ref: "#/components/schemas/error" + nullable: true + requirement: + description: The details of different requirements + type: object + properties: + endUser: + $ref: "#/components/schemas/endUserRequirement" + address: + $ref: "#/components/schemas/addressRequirement" + supportingDocuments: + description: List of all document requirements + type: array + items: + $ref: "#/components/schemas/supportingDocumentsRequirement" + endUserRequirement: + description: EndUser requirement specification + type: object + properties: + type: + $ref: "#/components/schemas/endUserTypeEnum" + fields: + description: List of all accepted end user fields + type: array + items: + $ref: "#/components/schemas/endUserField" + addressRequirement: + description: Address requirement specification + type: object + properties: + location: + $ref: "#/components/schemas/addressLocationEnum" + addressLocationEnum: + type: string + description: The address location + enum: + - LOCAL + - NATIONAL + - WORLDWIDE + example: NATIONAL + supportingDocumentsRequirement: + description: Document requirement specification + type: object + properties: + name: + description: The name of the document + type: string + example: Business registration + type: + $ref: "#/components/schemas/supportingDocumentTypeEnum" + description: + description: A small description about the document + type: string + example: The business registration document + acceptedDocuments: + description: List of document types accepted against the supporting document + type: array + items: + $ref: "#/components/schemas/documentTypeData" + supportingDocumentTypeEnum: + type: string + description: The type of the supporting document + enum: + - ADDRESS + - IDENTITY + example: ADDRESS + documentTypeData: + description: Document type specification + type: object + properties: + type: + description: Unique name indicating the type of the document + type: string + example: businessRegistration + friendlyName: + description: Human readable description of the document + type: string + example: Business registration + fields: + description: List of all accepted document fields + type: array + items: + $ref: "#/components/schemas/endUserField" + example: + - fieldName: companyName + friendlyName: Company Name + type: String + maxLength: 500 + - fieldName: issueDate + friendlyName: Issue Date + type: Date + fields: + type: object + description: Field is a key value pair of attribute name and value. All Date + or Number type values should be provided as a 'string'. Format for the Date + type is YYYY-MM-DD. + additionalProperties: + type: string + example: + firstName: First Name + issueDate: "2020-11-23" + validity: "120" + endUserField: + description: Field information + type: object + properties: + friendlyName: + type: string + description: Human readable description of the field + example: Salutation + fieldName: + type: string + description: Unique name of the field + example: salutation + type: + type: string + description: The data type of the field + example: String + values: + type: array + description: Provides accepted values for the field. Not Applicable when type value is 'Date'. + items: + type: string + example: + - Mr. + - Mrs. + required: + description: Specifies if the field is mandatory + type: boolean + example: true + maxLength: + description: Maximum allowed character length of the field. Not Applicable + when type value is 'Date' or values are provided. + type: integer + example: 10 + + + + numberActivationValidatorResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + $ref: "#/components/schemas/numberActivationValidatorResult" + errors: + $ref: "#/components/schemas/errors" + requirementsPackageHistoryResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + type: array + items: + $ref: "#/components/schemas/requirementsPackageHistoryData" + errors: + $ref: "#/components/schemas/errors" + requirementsPackageAssetsPostResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + $ref: "#/components/schemas/assetData" + errors: + $ref: "#/components/schemas/errors" + requirementsPackageListResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + type: array + items: + $ref: "#/components/schemas/requirementsPackageData" + errors: + $ref: "#/components/schemas/errors" + requirementsPackageAssetsGetResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + type: array + items: + $ref: "#/components/schemas/assetGetData" + errors: + $ref: "#/components/schemas/errors" + requirementsPackageCreateUpdateResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + $ref: "#/components/schemas/requirementsPackageData" + errors: + $ref: "#/components/schemas/errors" + requirementsPackageGetResponse: + type: object + properties: + links: + type: array + items: + $ref: "#/components/schemas/link" + data: + $ref: "#/components/schemas/requirementsPackageData" + errors: + $ref: "#/components/schemas/errors" + numberActivationValidatorResult: + description: | + Details of requirements for number activation validation. + Activatable indicates the TN is valid and can be provisioned or linked to a service. + Not-Activatable indicates the TN cannot be linked for activation. + type: object + properties: + requirementsSets: + type: array + items: + allOf: + - $ref: "#/components/schemas/numberActivationRequirementsData" + - type: object + properties: + status: + $ref: '#/components/schemas/requirementsPackageStatusEnum' + result: + description: Details of results of number activation validation + type: object + properties: + activatable: + $ref: "#/components/schemas/numberActivationResultData" + notActivatable: + $ref: "#/components/schemas/numberActivationResultData" + assetData: + description: Asset information with associated asset data + type: object + properties: + assetId: + description: Unique id of the asset + readOnly: true + type: string + example: 93ac73e8-20a7-95ec-bd32-4e8270e66c31 + assetType: + $ref: "#/components/schemas/assetTypeEnum" + assetReferenceId: + description: Id of the referenced asset object + type: string + example: 05ee3231-ad80-4fc5-9c13-68ffc26846a5 + assetReferenceData: + description: Details of the referenced asset object. It can corresponds + to an address, endUser or document + readOnly: true + oneOf: + - $ref: "#/components/schemas/createAddressRequestData" + - $ref: "#/components/schemas/complianceDocumentResponseData" + - $ref: "#/components/schemas/endUserData" + createdDateTime: + allOf: + - $ref: "#/components/schemas/createdDateTime" + readOnly: true + updatedDateTime: + allOf: + - $ref: "#/components/schemas/updatedDateTime" + readOnly: true + assetTypeEnum: + type: string + description: Type of the asset + enum: + - ADDRESS + - END_USER + - DOCUMENT + example: ADDRESS + assetGetData: + description: Asset information with associated asset data + type: object + properties: + assetId: + description: Unique id of the asset + readOnly: true + type: string + example: 93ac73e8-20a7-95ec-bd32-4e8270e66c31 + assetReferenceId: + description: Id of the referenced asset object + type: string + example: 05ee3231-ad80-4fc5-9c13-68ffc26846a5 + assetType: + $ref: "#/components/schemas/assetTypeEnum" + assetReferenceData: + description: Details of the referenced asset object. It can corresponds + to an address, endUser or document + readOnly: true + oneOf: + - $ref: "#/components/schemas/addressResponseAssetData" + - $ref: "#/components/schemas/endUserResponseAssetData" + - $ref: "#/components/schemas/complianceDocumentResponseData" + createdDateTime: + allOf: + - $ref: "#/components/schemas/createdDateTime" + readOnly: true + updatedDateTime: + allOf: + - $ref: "#/components/schemas/updatedDateTime" + readOnly: true + requirementsPackageHistoryData: + description: History of the Requirements package + type: object + properties: + status: + $ref: "#/components/schemas/requirementsPackageStatusEnum" + customReference: + type: string + description: The customReference of the requirements package + email: + type: string + format: email + description: The email provided as part of the requirements + callbacks: + type: string + format: uri + description: The callback provided as part of the requirements + dateTime: + type: string + format: date-time + description: The date and time of the requirements package event in ISO 8601 format + example: "2015-03-11T04:09:25.399Z" + author: + type: string + description: The name of the user who performed the requirements package event + example: Bandwidth User + remarks: + type: string + description: The remarks, if any, provided by the author along with the requirements package event + maxLength: 5000 + example: Remarks provided by user + allDetailsAccurate: + type: boolean + description: The confirmation on accuracy of data submitted at the time of the requirements package event + example: true + requirementsPackageStatusEnum: + type: string + description: Status of the Requirements package + enum: + - DRAFT + - SUBMITTED + - VERIFIED + - VERIFICATION_FAILED + - DISABLED + - AUTO_VALIDATED + example: SUBMITTED + requirementsPackageData: + description: Details of the Requirements package + type: object + properties: + requirementsPackageId: + allOf: + - $ref: "#/components/schemas/requirementsPackageId" + readOnly: true + countryCodeA3: + $ref: "#/components/schemas/countryCodeA3String" + phoneNumberType: + $ref: "#/components/schemas/phoneNumberTypeEnum" + endUserType: + $ref: "#/components/schemas/endUserTypeEnum" + customReference: + $ref: "#/components/schemas/customReference" + email: + $ref: "#/components/schemas/email" + callback: + $ref: "#/components/schemas/callbackUrl" + acknowledgements: + $ref: "#/components/schemas/requirementsPackageAcknowledgements" + status: + allOf: + - $ref: '#/components/schemas/requirementsPackageStatusEnum' + readOnly: true + createdDateTime: + allOf: + - $ref: "#/components/schemas/createdDateTime" + readOnly: true + updatedDateTime: + allOf: + - $ref: "#/components/schemas/updatedDateTime" + readOnly: true + remarks: + $ref: "#/components/schemas/remarks" + numberActivationResultData: + description: Details of result of a number activation + type: object + properties: + areaCodes: + type: array + items: + $ref: "#/components/schemas/areaCode" + reasons: + type: array + items: + type: string + remarks: + type: string + description: Remarks provided by the user or admin + maxLength: 5000 + readOnly: true + example: Remarks provided by Admin + callbackUrl: + description: Callback URL + type: string + example: http://somehost.com/new-callback + email: + type: string + format: email + minLength: 0 + maxLength: 500 + pattern: + ^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$ + example: foo@bar.com + requirementsPackageAcknowledgements: + description: Captures the acknowledgments on requirements package submission. Mandatory when status is provided + type: object + properties: + allDetailsAccurate: + type: boolean + description: Confirms that all provided information is accurate and valid + for the end user attached to the requirements package. + example: true + addressResponseAssetData: + type: object + properties: + addressId: + $ref: "#/components/schemas/addressId" + city: + $ref: "#/components/schemas/addressCity" + countryCodeA3: + $ref: "#/components/schemas/countryCodeA3" + postalCode: + $ref: "#/components/schemas/postalCode" + addressLine1: + $ref: "#/components/schemas/addressLine1" + addressLine2: + $ref: "#/components/schemas/addressLine2" + stateAbbreviation: + $ref: "#/components/schemas/stateAbbreviation" + customReference: + $ref: "#/components/schemas/customReference" + geoValidationStatus: + $ref: "#/components/schemas/geoValidationStatusEnum" + createdDateTime: + $ref: "#/components/schemas/createdDateTime" + updatedDateTime: + $ref: "#/components/schemas/updatedDateTime" + endUserResponseAssetData: + type: object + properties: + type: + $ref: "#/components/schemas/endUserTypeEnum" + customReference: + $ref: "#/components/schemas/customReference" + fields: + $ref: "#/components/schemas/fields" + endUserId: + $ref: "#/components/schemas/endUserId" + status: + $ref: "#/components/schemas/endUserStatusEnum" + createdDateTime: + $ref: "#/components/schemas/createdDateTime" + updatedDateTime: + $ref: "#/components/schemas/updatedDateTime" + numberActivationRequirementsSet: + description: A set of number activation requirements + type: object + properties: + requirementsSets: + type: array + items: + $ref: "#/components/schemas/numberActivationRequirementsData" + required: + - requirementsSets + numberActivationRequirementsData: + description: Details of requirements for number activation + type: object + properties: + requirementsPackageId: + $ref: "#/components/schemas/requirementsPackageId" + countryCodeA3: + $ref: "#/components/schemas/countryCodeA3String" + phoneNumberType: + $ref: "#/components/schemas/phoneNumberTypeEnum" + referenceId: + type: string + description: A unique reference id to the requirement set + example: requirementsSetReference + areaCodes: + type: array + items: + $ref: "#/components/schemas/areaCode" + example: + - '454' + - '456' + - '794' + - '796' + required: + - requirementsPackageId + - countryCodeA3 + - phoneNumberType + - areaCodes + areaCode: + type: string + description: The 1 to 6 digit area code associated with the phone number. + example: "435" + requirementsPackageId: + type: string + description: Unique identifier for requirements package + example: 93ac73e8-20a7-95ec-bd32-4e8270e66c31 + examples: + geoValidationAddressSuggestionExample: + summary: Example of Geo-validation suggestion address + value: + links: [] + data: null + errors: + - description: | + Address provided is not geo-valid. + Please refer suggestedAddresses for a list of geo valid addresses + code: 205617 + meta: + suggestedAddresses: + - countryCodeA3: USA + addressLine1: Main Street, 123 + city: Seattle + stateAbbreviation: CA + postalCode: "67890" + - countryCodeA3: USA + addressLine1: 456 Elm Street + city: Seattle + stateAbbreviation: NC + postalCode: 67890 + geoValidationDisabledExample: + summary: Example of Geo-validation not enabled on account + value: + links: [] + data: null + errors: + - type: bad-request + description: Geo-validation is not enabled for this account. Please contact customer support. + code: 20011 + badRequestErrorExample: + summary: Bad Request Error Example + value: + links: [] + data: null + errors: + - type: bad-request + description: There was an issue with your request. + code: 205617 + unauthorizedErrorExample: + summary: Unauthorized Error Example + value: + links: [] + data: null + errors: + - type: authentication-credentials + description: Invalid or missing credentials. + code: 206401 + forbiddenErrorExample: + summary: Unauthorized Error Example + value: + links: [] + data: null + errors: + - type: resource-permissions + description: User does not have permissions to access this resource. + code: 109107 + notFoundErrorExample: + summary: Not Found Error Example + value: + links: [] + data: null + errors: + - type: resource-not-found + description: The resource specified cannot be found. + code: 50420 + methodNotAllowedErrorExample: + summary: An example of a generic Not Allowed Error Example + value: + links: [] + data: null + errors: + - type: http-method-not-supported + description: The HTTP method used is not supported by this resource. + code: 205621 + meta: + method: TRACE + tooManyRequestsErrorExample: + summary: Too Many Requests Error Example + value: + links: [] + data: null + errors: + - type: rate-limiting + description: | + Rate limit exceeded. Wait for the time specified in the "Retry-After" + header before sending another request. + code: 32002 + internalServerErrorExample: + summary: Internal Server Error Example + value: + links: [] + data: null + errors: + - type: internal-server-error + description: | + Unexpected internal server error. Contact Bandwidth Customer + Support if this problem persists. + code: 32030 + patchEndUserBodyBasicExample: + summary: Update customReference, add or modify fields + value: + customReference: "New custom reference" + fields: + companyVat: "AB123456" + dateOfBirth: "1992-11-23" + firstName: "First Name" + postRequirementsPackageAssetBodyBasicExample: + summary: Attach an asset to requirement package + value: + assetReferenceId: 05ee3231-ad80-4fc5-9c13-68ffc26846a5 + assetType: ADDRESS + requirementsPackageResponseExample: + summary: Requirements Package Response Example + value: + links: + - href: https://api.bandwidth.com/api/v2/accounts/1/compliance/requirementsPackages/46bd4a91-582d-442f-bd1b-bcf17ca7fc6a + rel: self + method: GET + data: + requirementsPackageId: 93ac73e8-20a7-95ec-bd32-4e8270e66c31 + countryCodeA3: FRA + phoneNumberType: GEOGRAPHIC + endUserType: BUSINESS + customReference: home_office + email: foo@bar.com + callback: http://somehost.com/new-callback + acknowledgements: + allDetailsAccurate: true + status: SUBMITTED + createdDateTime: "2024-03-11T04:09:25.399Z" + updatedDateTime: "2024-03-11T04:09:25.399Z" + remarks: Remarks provided by Admin + errors: [] + securitySchemes: + Basic: + type: http + scheme: basic + description: |- + Basic authentication is a simple authentication scheme built into the HTTP protocol. To use it, send your HTTP requests with an `Authorization` header that contains the word `Basic` followed by a space and a Base64-encoded string `username:password`. + + - Example: `Authorization: Basic ZGVtbZpwQDU1dzByZA==` + Bearer: + type: http + scheme: bearer + bearerFormat: JWT + description: |- + Bearer authentication (also called token authentication) is an HTTP authentication scheme that involves security tokens called bearer tokens. The name “Bearer authentication” can be understood as “give access to the bearer of this token.” The client must send this token in the `Authorization` header when making requests to protected resources. + + - Example: `Authorization: Bearer ` + + Where `` should be replaced with the token string without angle brackets. + + For more information, see the following guide: [Credentials](https://dev.bandwidth.com/docs/credentials) +tags: + - name: Addresses + - name: Compliance + - name: Requirements Packages +security: + - Basic: [] + - Bearer: [] diff --git a/test/test_servers.py b/test/test_servers.py index 4ec8969..5be739e 100644 --- a/test/test_servers.py +++ b/test/test_servers.py @@ -17,7 +17,7 @@ async def create_mcp_server(name=None, tools=None, excluded_tools=None): return mcp -def calculate_expected_tools(tools, excluded_tools, total_tools=19): +def calculate_expected_tools(tools, excluded_tools, total_tools=46): if tools and not excluded_tools: return len(tools) elif excluded_tools: @@ -43,7 +43,7 @@ async def test_full_mcp_server_creation(tools, excluded_tools, httpx_mock: HTTPX expected_tools = calculate_expected_tools(tools, excluded_tools) name = f"Test MCP with {expected_tools} Tools" - for name in ["messaging", "multi-factor-auth", "phone-number-lookup", "insights"]: + for name in ["messaging", "multi-factor-auth", "phone-number-lookup", "insights", "end-user-management"]: create_mock(httpx_mock, name) mcp = await create_mcp_server(name, tools, excluded_tools) From bc6aaec7227fab6e01b72935d807b1eb312e283d Mon Sep 17 00:00:00 2001 From: ckoegel Date: Fri, 22 Aug 2025 16:39:43 -0400 Subject: [PATCH 23/33] update use cases guide --- common_use_cases.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/common_use_cases.md b/common_use_cases.md index a6dd2f3..0c1b648 100644 --- a/common_use_cases.md +++ b/common_use_cases.md @@ -11,6 +11,8 @@ If you're looking to send messages using the MCP server, we recommend enabling t - `createMessage` - Send SMS or MMS messages - `createMultiChannelMessage` - Send multi-channel messages (mostly for RBM messaging) +Sending messages requires `BW_ACCOUNT_ID`, `BW_MESSAGING_APPLICATION_ID`, and `BW_NUMBER` to be set in your environment variables. + **Enabling these tools** ```sh # Environment Variable @@ -28,6 +30,8 @@ Most agents we've experimented with have been smart enough to figure out that yo need to both create a lookup request and then get its' status to actually get the TN info, and enabling only these two tools is a good way to help your agent remember that! +Phone Number Lookup requires the `BW_ACCOUNT_ID` environment variable to be set. + **Enabling these tools** ```sh # Environment Variable @@ -44,6 +48,9 @@ To create and verify multi-factor authentication codes, you'll need our three MF - `generateVoiceCode` - Use to generate and send an MFA code via a phone call - `verifyCode` - Verify an MFA code sent with one of the previous tools +Generating Messaging and Voice codes requires the `BW_MESSAGING_APPLICATION_ID` and `BW_VOICE_APPLICATION_ID` environment variables respectively. +Both of these tools also require `BW_ACCOUNT_ID` and `BW_NUMBER`. + **Enabling these tools** ```sh # Environment Variable From 8781b7e88821b4ab426aba38ac5570a75be42e68 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Fri, 22 Aug 2025 16:56:03 -0400 Subject: [PATCH 24/33] update config --- src/config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/config.py b/src/config.py index eadfa75..106f623 100644 --- a/src/config.py +++ b/src/config.py @@ -14,17 +14,17 @@ def load_config() -> Dict[str, str]: "BW_VOICE_APPLICATION_ID", ] - # Required variables - for var in required_vars: - if var not in os.environ: - raise ValueError(f"Missing required environment variable: {var}") - # Add all variables that exist for var in required_vars + optional_vars: value = os.getenv(var) if value: config[var] = value + # Required variables + for var in required_vars: + if var not in config.keys(): + raise ValueError(f"Missing required environment variable: {var}") + return config From 33d3218562a8aa90bade3402d4e067020b9db00e Mon Sep 17 00:00:00 2001 From: ckoegel Date: Tue, 26 Aug 2025 14:44:48 -0400 Subject: [PATCH 25/33] readme updates --- README.md | 87 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 7804674..f7a1323 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ The following variables are optional or conditionally required: ```sh BW_ACCOUNT_ID # Your Bandwidth Account ID. Required for most API operations. -BW_NUMBER # A valid phone number on your Bandwidth account. Used with our Messaging and MFA APIs. +BW_NUMBER # A valid phone number on your Bandwidth account. Used with our Messaging and MFA APIs. Must be in E164 format. BW_MESSAGING_APPLICATION_ID # A Bandwidth Messaging Application ID. Used with our Messaging and MFA APIs. BW_VOICE_APPLICATION_ID # A Bandwidth Voice Application ID. Used with our MFA API. BW_MCP_TOOLS # The list of MCP tools you'd like to enable. If not set, all tools are enabled. @@ -89,34 +89,10 @@ BW_MCP_EXCLUDE_TOOLS=createLookup,getLookupStatus Below you'll find instructions for using our MCP server with different common AI agents, as well as instructions for running the server locally. For usage with AI agents, it is recommended to use a combination of [uv](https://github.com/astral-sh/uv?tab=readme-ov-file#uv) and environment variables to start and configure the server respectively. -### Claude Desktop - -1. Install [Claude Desktop](https://claude.ai/download) -2. Edit your `claude_desktop_config.json` to include the following object - -```json -{ - "mcpServers": { - "Bandwidth": { - "command": "uvx", - "args": ["--from", "/path/to/bandwidth-mcp-server", "start"], - "env": { - "BW_USERNAME": "", - "BW_PASSWORD": "", - "BW_MCP_TOOLS": "tools,to,enable", - "BW_MCP_EXCLUDE_TOOLS": "tools,to,exclude", - } - } - } -} -``` - -> **_NOTE:_** You can also run the server directly from our github repo by replacing -`/path/to/bandwidth-mcp-server` with: `git+https://github.com/Bandwidth/bandwidth-mcp-server.git` - ### Goose CLI 1. Install [Goose CLI](https://block.github.io/goose/docs/getting-started/installation/) + - We recommend configuring Goose to use `Allow Mode`. This will require user approval before Goose calls tools, which could prevent Goose from accidentally taking unwanted actions. 2. Add the Bandwidth MCP Server as a Command-line Extension ```shell @@ -165,9 +141,37 @@ Then follow the prompts like the example below. } ``` +### Claude Desktop + +1. Install [Claude Desktop](https://claude.ai/download) +2. Edit your `claude_desktop_config.json` to include the following object + +```json +{ + "mcpServers": { + "Bandwidth": { + "command": "uvx", + "args": ["--from", "/path/to/bandwidth-mcp-server", "start"], + "env": { + "BW_USERNAME": "", + "BW_PASSWORD": "", + "BW_MCP_TOOLS": "tools,to,enable", + "BW_MCP_EXCLUDE_TOOLS": "tools,to,exclude", + } + } + } +} +``` + +> **_NOTE:_** You can also run the server directly from our github repo by replacing +`/path/to/bandwidth-mcp-server` with: `git+https://github.com/Bandwidth/bandwidth-mcp-server.git` + +> **_NOTE:_** We've noticed some issues with Claude not being able to see MCP resources. This could require you to enter some tool parameters normally included in our config resource manually. + ### Running the Server Standalone The MCP server can be run locally using either native python or uv. +When running this way, all environment variables MUST be set in your system environment. #### Run Using Native Python @@ -232,3 +236,34 @@ uvx --from ./ start - `listMessages` - List messages with filtering options - `createMessage` - Send SMS/MMS messages - `createMultiChannelMessage` - Send multi-channel messages (RBM, SMS, MMS) + +## **Address Management** +- `getAddressFields` - Get supported address fields by country +- `validateAddress` - Validate an address and get excluded features +- `listAddresses` - List all addresses +- `createAddress` - Create an address +- `getAddress` - Get an address by ID +- `updateAddress` - Update an address +- `listCityInfo` - List city info search results + +## **Compliance & Requirements** +- `listDocumentTypes` - List all accepted document types and metadata requirements +- `listEndUserTypes` - List all End user types and accepted metadata +- `listEndUserActivationRequirements` - List requirements for End user activation +- `getComplianceDocumentMetadata` - Get metadata of uploaded documents +- `updateComplianceDocument` - Modify document data and file +- `downloadComplianceDocuments` - Download document using document ID +- `createComplianceDocument` - Upload a document with metadata +- `listComplianceEndUsers` - List all End users of an account +- `createComplianceEndUser` - Create an End user +- `getComplianceEndUser` - Retrieve an End User by ID +- `updateComplianceEndUser` - Update End user details +- `listRequirementsPackages` - List all requirements packages +- `createRequirementsPackage` - Create a requirements package +- `getRequirementsPackage` - Retrieve a requirements package +- `patchRequirementsPackage` - Update Requirements package +- `getRequirementsPackageAssets` - Get assets attached to requirements package +- `attachRequirementsPackageAsset` - Attach an asset to requirements package +- `detachRequirementsPackageAsset` - Detach an asset from requirements package +- `validateNumberActivation` - Validate number activation requirements +- `getRequirementsPackageHistory` - Get history of a requirements package From bdce27e3b84fd8a111ca24c8ec72980606afa481 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Tue, 26 Aug 2025 14:46:58 -0400 Subject: [PATCH 26/33] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f7a1323..a1ce81e 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ Then follow the prompts like the example below. > **_NOTE:_** You can also run the server directly from our github repo by replacing `/path/to/bandwidth-mcp-server` with: `git+https://github.com/Bandwidth/bandwidth-mcp-server.git` -> **_NOTE:_** We've noticed some issues with Claude not being able to see MCP resources. This could require you to enter some tool parameters normally included in our config resource manually. +> **_NOTE:_** We've noticed some issues with Claude not being able to see MCP resources. This could require you to manually enter some tool parameters normally included in our config resource. ### Running the Server Standalone From f095a5cfaf1b5d552db40942784ff3bd8f27c02e Mon Sep 17 00:00:00 2001 From: ckoegel Date: Fri, 29 Aug 2025 12:20:54 -0400 Subject: [PATCH 27/33] readme and use cases updates --- README.md | 41 ++++++++++++++++++++++++++++++++++++++--- common_use_cases.md | 29 +++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a1ce81e..0912720 100644 --- a/README.md +++ b/README.md @@ -128,8 +128,8 @@ Then follow the prompts like the example below. { "mcpServers": { "bw-mcp-server": { - "command": "/Users/ckoegel/Documents/repositories/sdks/bandwidth-mcp-server/.venv/bin/python", - "args": ["/Users/ckoegel/Documents/repositories/sdks/bandwidth-mcp-server/src/app.py"], + "command":"uvx", + "args": ["--from", "/path/to/bandwidth-mcp-server", "start"], "env": { "BW_USERNAME": "", "BW_PASSWORD": "", @@ -141,6 +141,39 @@ Then follow the prompts like the example below. } ``` +### VSCode (Copilot) + +1. Within VSCode, open the Command Palette and search for `MCP: Add Server`. +2. Choose `Command (stdio)`, then enter the full command to start the server. (Example Below) + +```shell +uvx --from /path/to/bandwidth-mcp-server start +``` + +3. Choose a name for the server (ie. `bw-mcp-server`) and select if you'd like it to be enabled Globally or only in the current workspace. +4. You can configure environment variables by opening the `mcp.json` file VSCode provides like the example below. + +```json +{ + "servers": { + "bw-mcp-server": { + "type": "stdio", + "command": "uvx", + "args": ["--from", "/path/to/bandwidth-mcp-server", "start"], + "env": { + "BW_USERNAME": "", + "BW_PASSWORD": "", + "BW_MCP_TOOLS": "tools,to,enable", + "BW_MCP_EXCLUDE_TOOLS": "tools,to,exclude", + } + } + }, + "inputs": [] +} +``` + +> **_NOTE:_** You may need to make sure MCP servers are enabled in VSCode to begin using the server. See the [official guide](https://code.visualstudio.com/docs/copilot/customization/mcp-servers) for more info. + ### Claude Desktop 1. Install [Claude Desktop](https://claude.ai/download) @@ -246,7 +279,7 @@ uvx --from ./ start - `updateAddress` - Update an address - `listCityInfo` - List city info search results -## **Compliance & Requirements** +## **Compliance** - `listDocumentTypes` - List all accepted document types and metadata requirements - `listEndUserTypes` - List all End user types and accepted metadata - `listEndUserActivationRequirements` - List requirements for End user activation @@ -258,6 +291,8 @@ uvx --from ./ start - `createComplianceEndUser` - Create an End user - `getComplianceEndUser` - Retrieve an End User by ID - `updateComplianceEndUser` - Update End user details + +## **Requirements Packages** - `listRequirementsPackages` - List all requirements packages - `createRequirementsPackage` - Create a requirements package - `getRequirementsPackage` - Retrieve a requirements package diff --git a/common_use_cases.md b/common_use_cases.md index 0c1b648..5ee6d06 100644 --- a/common_use_cases.md +++ b/common_use_cases.md @@ -59,3 +59,32 @@ BW_MCP_TOOLS=generateMessagingCode,generateVoiceCode,verifyCode # CLI Flag --tools generateMessagingCode,generateVoiceCode,verifyCode ``` + +## Adding a Business End User + +To add an end user, you'll need three specific Compliance endpoints. +- `listEndUserTypes` - Used to list all End user types and their required fields +- `listEndUserActivationRequirements` - Required if the end user will be used for requirements packages +- `createComplianceEndUser` - Used to create the end user + +These tools will require the `BW_ACCOUNT_ID` environment variable. + +**Enabling these tools** +```sh +# Environment Variable +BW_MCP_TOOLS=listEndUserTypes,listEndUserActivationRequirements,createComplianceEndUser +# CLI Flag +--tools listEndUserTypes,listEndUserActivationRequirements,createComplianceEndUser +``` + +## Address Validation + +Validating an address can be done with just the `validateAddress` tool and the `BW_ACCOUNT_ID` environment variable! + +**Enabling this tool** +```sh +# Environment Variable +BW_MCP_TOOLS=validateAddress +# CLI Flag +--tools validateAddress +``` From 9c6c0b648901d74bb72f85172ff07b5ebb32d857 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Fri, 29 Aug 2025 15:19:28 -0400 Subject: [PATCH 28/33] update test command in wf --- .github/workflows/test-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-pr.yml b/.github/workflows/test-pr.yml index 0f43eca..484f046 100644 --- a/.github/workflows/test-pr.yml +++ b/.github/workflows/test-pr.yml @@ -35,4 +35,4 @@ jobs: run: | pip install -r requirements.txt pip install -r dev-requirements.txt - pytest -v + python -m pytest -v From 38c3a14af61a4671b62ed8366bef72e3ed93e040 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Fri, 29 Aug 2025 15:22:12 -0400 Subject: [PATCH 29/33] add pytest async req --- dev-requirements.txt | 1 + pyproject.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index 644d34f..7dac1cb 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,4 @@ pytest>=8.4.1 +pytest-asyncio>=1.1.0 pytest-httpx>=0.35.0 black>=25.1.0 diff --git a/pyproject.toml b/pyproject.toml index 02e7e08..0c9356f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,5 +20,6 @@ start = "app:main" dev = [ "black>=25.1.0", "pytest>=8.4.1", + "pytest-asyncio>=1.1.0", "pytest-httpx>=0.35.0", ] From 7d8bfa368c62e558510a30091a0389cb79e33947 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Fri, 29 Aug 2025 15:30:54 -0400 Subject: [PATCH 30/33] add encoding to file open for mocks --- test/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils.py b/test/utils.py index 2e10d98..b2ec6a9 100644 --- a/test/utils.py +++ b/test/utils.py @@ -3,7 +3,7 @@ def create_mock(httpx_mock: HTTPXMock, spec_name: str): """Helper function to create a mock response for HTTPX.""" - with open(f"test/fixtures/{spec_name}.yml", "r") as f: + with open(f"test/fixtures/{spec_name}.yml", "r", encoding="utf-8") as f: response_text = f.read() httpx_mock.add_response( url=f"https://dev.bandwidth.com/spec/{spec_name}.yml", text=response_text From ead710adb6fed0b03f0754adb6d179cac2d4e084 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Fri, 29 Aug 2025 15:32:36 -0400 Subject: [PATCH 31/33] format --- src/config.py | 2 +- src/servers.py | 4 +++- test/test_servers.py | 8 +++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/config.py b/src/config.py index 106f623..ed01d7c 100644 --- a/src/config.py +++ b/src/config.py @@ -24,7 +24,7 @@ def load_config() -> Dict[str, str]: for var in required_vars: if var not in config.keys(): raise ValueError(f"Missing required environment variable: {var}") - + return config diff --git a/src/servers.py b/src/servers.py index 9927fe2..4a5a6a8 100644 --- a/src/servers.py +++ b/src/servers.py @@ -19,7 +19,9 @@ "url": "https://dev.bandwidth.com/spec/phone-number-lookup.yml" }, "insights": {"url": "https://dev.bandwidth.com/spec/insights.yml"}, - "end-user-management": {"url": "https://dev.bandwidth.com/spec/end-user-management.yml"} + "end-user-management": { + "url": "https://dev.bandwidth.com/spec/end-user-management.yml" + }, } diff --git a/test/test_servers.py b/test/test_servers.py index 5be739e..b84b021 100644 --- a/test/test_servers.py +++ b/test/test_servers.py @@ -43,7 +43,13 @@ async def test_full_mcp_server_creation(tools, excluded_tools, httpx_mock: HTTPX expected_tools = calculate_expected_tools(tools, excluded_tools) name = f"Test MCP with {expected_tools} Tools" - for name in ["messaging", "multi-factor-auth", "phone-number-lookup", "insights", "end-user-management"]: + for name in [ + "messaging", + "multi-factor-auth", + "phone-number-lookup", + "insights", + "end-user-management", + ]: create_mock(httpx_mock, name) mcp = await create_mcp_server(name, tools, excluded_tools) From fc4b29daf7419805f855b701c938852540621799 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Tue, 2 Sep 2025 09:36:47 -0400 Subject: [PATCH 32/33] update links to repo --- .bandwidth/catalog-info.yaml | 6 +++--- .bandwidth/component.yaml | 4 ++-- README.md | 16 ++++++++-------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.bandwidth/catalog-info.yaml b/.bandwidth/catalog-info.yaml index 4c414dd..862f4b7 100644 --- a/.bandwidth/catalog-info.yaml +++ b/.bandwidth/catalog-info.yaml @@ -2,10 +2,10 @@ apiVersion: backstage.io/v1alpha1 kind: Location metadata: schemaVersion: v1.0.0 - name: bandwidth-mcp-server-location - description: Links to additional entities in the bandwidth-mcp-server repository. + name: mcp-server-location + description: Links to additional entities in the mcp-server repository. annotations: - github.com/project-slug: Bandwidth/bandwidth-mcp-server + github.com/project-slug: Bandwidth/mcp-server spec: targets: - ./component.yaml diff --git a/.bandwidth/component.yaml b/.bandwidth/component.yaml index 295a747..b5efbd7 100644 --- a/.bandwidth/component.yaml +++ b/.bandwidth/component.yaml @@ -2,10 +2,10 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: schemaVersion: v1.0.0 - name: bandwidth-mcp-server + name: mcp-server description: Official Bandwidth MCP Server annotations: - github.com/project-slug: Bandwidth/bandwidth-mcp-server + github.com/project-slug: Bandwidth/mcp-server organization: BW costCenter: Development - Software Infra platformType: diff --git a/README.md b/README.md index 0912720..b7db01c 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ The server is provided as a python package and may be cloned directly from this Clone directly from this git repository using: ```shell -git clone https://github.com/Bandwidth/bandwidth-mcp-server.git -cd bandwidth-mcp-server +git clone https://github.com/Bandwidth/mcp-server.git +cd mcp-server ``` ## Getting Started @@ -114,7 +114,7 @@ Then follow the prompts like the example below. │ bw-mcp-server │ ◇ What command should be run? -│ uvx --from /path/to/bandwidth-mcp-server start +│ uvx --from /path/to/mcp-server start ``` > **_NOTE:_** If you configure environment variables with Goose, it will prioritize those over your system environment variables. @@ -129,7 +129,7 @@ Then follow the prompts like the example below. "mcpServers": { "bw-mcp-server": { "command":"uvx", - "args": ["--from", "/path/to/bandwidth-mcp-server", "start"], + "args": ["--from", "/path/to/mcp-server", "start"], "env": { "BW_USERNAME": "", "BW_PASSWORD": "", @@ -147,7 +147,7 @@ Then follow the prompts like the example below. 2. Choose `Command (stdio)`, then enter the full command to start the server. (Example Below) ```shell -uvx --from /path/to/bandwidth-mcp-server start +uvx --from /path/to/mcp-server start ``` 3. Choose a name for the server (ie. `bw-mcp-server`) and select if you'd like it to be enabled Globally or only in the current workspace. @@ -159,7 +159,7 @@ uvx --from /path/to/bandwidth-mcp-server start "bw-mcp-server": { "type": "stdio", "command": "uvx", - "args": ["--from", "/path/to/bandwidth-mcp-server", "start"], + "args": ["--from", "/path/to/mcp-server", "start"], "env": { "BW_USERNAME": "", "BW_PASSWORD": "", @@ -184,7 +184,7 @@ uvx --from /path/to/bandwidth-mcp-server start "mcpServers": { "Bandwidth": { "command": "uvx", - "args": ["--from", "/path/to/bandwidth-mcp-server", "start"], + "args": ["--from", "/path/to/mcp-server", "start"], "env": { "BW_USERNAME": "", "BW_PASSWORD": "", @@ -197,7 +197,7 @@ uvx --from /path/to/bandwidth-mcp-server start ``` > **_NOTE:_** You can also run the server directly from our github repo by replacing -`/path/to/bandwidth-mcp-server` with: `git+https://github.com/Bandwidth/bandwidth-mcp-server.git` +`/path/to/mcp-server` with: `git+https://github.com/Bandwidth/mcp-server.git` > **_NOTE:_** We've noticed some issues with Claude not being able to see MCP resources. This could require you to manually enter some tool parameters normally included in our config resource. From 84543fd1c9de5caa3e6503f49e646561b4b27986 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Tue, 2 Sep 2025 09:38:08 -0400 Subject: [PATCH 33/33] remove untested hint --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index b7db01c..6c7ef0b 100644 --- a/README.md +++ b/README.md @@ -196,9 +196,6 @@ uvx --from /path/to/mcp-server start } ``` -> **_NOTE:_** You can also run the server directly from our github repo by replacing -`/path/to/mcp-server` with: `git+https://github.com/Bandwidth/mcp-server.git` - > **_NOTE:_** We've noticed some issues with Claude not being able to see MCP resources. This could require you to manually enter some tool parameters normally included in our config resource. ### Running the Server Standalone