mcpify-cli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Amanpreet Singh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcpify-cli
3
+ Version: 0.1.0
4
+ Summary: Turn any OpenAPI/REST API into an MCP server in one command.
5
+ Project-URL: Homepage, https://github.com/Amanbig/mcpify
6
+ Project-URL: Issues, https://github.com/Amanbig/mcpify/issues
7
+ Author: Amanpreet Singh
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: ai-agents,claude,mcp,model-context-protocol,openapi,rest
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: httpx>=0.27
13
+ Requires-Dist: mcp>=1.2.0
14
+ Requires-Dist: pyyaml>=6.0
15
+ Description-Content-Type: text/markdown
16
+
17
+ # mcpify
18
+
19
+ **Turn any REST API into an MCP server in one command.** Point `mcpify` at an OpenAPI
20
+ spec and every endpoint becomes a tool your AI agent (Claude, Cursor, Windsurf, …) can
21
+ call — no glue code, no per-endpoint wrappers.
22
+
23
+ ```bash
24
+ mcpify https://api.example.com/openapi.json
25
+ ```
26
+
27
+ That's it. Every operation in the spec is now a live MCP tool.
28
+
29
+ ---
30
+
31
+ ## Why
32
+
33
+ [MCP](https://modelcontextprotocol.io) is how AI agents call real tools. But wiring an
34
+ existing API into MCP means hand-writing a tool wrapper for every endpoint — tedious and
35
+ stale the moment the API changes. Almost every API already publishes an **OpenAPI spec**.
36
+ `mcpify` reads that spec and generates the whole MCP server at runtime, so:
37
+
38
+ - **Zero per-endpoint code** — N endpoints → N tools, automatically.
39
+ - **Always in sync** — regenerate from the spec; no wrappers to maintain.
40
+ - **Auth-aware** — pass an API key / bearer token once, applied to every call.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pipx install mcpify-cli # or: uvx mcpify-cli ...
46
+ ```
47
+
48
+ ## Use
49
+
50
+ Preview the tools a spec produces (no server):
51
+
52
+ ```bash
53
+ mcpify --list examples/petstore-mini.yaml
54
+ ```
55
+
56
+ Run it as an MCP server (stdio):
57
+
58
+ ```bash
59
+ mcpify https://petstore3.swagger.io/api/v3/openapi.json
60
+ mcpify ./openapi.yaml --base-url https://staging.internal.api
61
+ mcpify ./openapi.yaml --auth "Authorization: Bearer $TOKEN" # or set MCPIFY_AUTH
62
+ ```
63
+
64
+ ### Plug it into Claude
65
+
66
+ Add to your MCP client config (Claude Desktop / Claude Code `mcp.json`):
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "petstore": {
72
+ "command": "mcpify",
73
+ "args": ["https://petstore3.swagger.io/api/v3/openapi.json"]
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ Restart the client and ask: *"list the available pets"* — Claude calls the API directly.
80
+
81
+ ## How it works
82
+
83
+ 1. Load the OpenAPI 3.x document (URL or file, JSON or YAML).
84
+ 2. Walk every path/method into an `Operation` with a generated JSON input schema.
85
+ 3. Serve them over MCP; each tool call is mapped to a live HTTP request (path, query,
86
+ header params and JSON bodies all handled) and the response is returned to the agent.
87
+
88
+ ## Supported
89
+
90
+ - OpenAPI 3.x, JSON or YAML, from a URL or local file
91
+ - `GET` / `POST` / `PUT` / `PATCH` / `DELETE`
92
+ - Path, query, and header parameters; JSON request bodies
93
+ - A single global auth header (API key or bearer token)
94
+
95
+ ## Roadmap
96
+
97
+ - Per-operation filtering (`--only`, `--tag`) to expose a subset of a large API
98
+ - `$ref` resolution for fully-expanded body schemas
99
+ - Swagger 2.0 specs
100
+ - SSE / HTTP transport in addition to stdio
101
+
102
+ PRs welcome.
103
+
104
+ ## License
105
+
106
+ MIT
@@ -0,0 +1,90 @@
1
+ # mcpify
2
+
3
+ **Turn any REST API into an MCP server in one command.** Point `mcpify` at an OpenAPI
4
+ spec and every endpoint becomes a tool your AI agent (Claude, Cursor, Windsurf, …) can
5
+ call — no glue code, no per-endpoint wrappers.
6
+
7
+ ```bash
8
+ mcpify https://api.example.com/openapi.json
9
+ ```
10
+
11
+ That's it. Every operation in the spec is now a live MCP tool.
12
+
13
+ ---
14
+
15
+ ## Why
16
+
17
+ [MCP](https://modelcontextprotocol.io) is how AI agents call real tools. But wiring an
18
+ existing API into MCP means hand-writing a tool wrapper for every endpoint — tedious and
19
+ stale the moment the API changes. Almost every API already publishes an **OpenAPI spec**.
20
+ `mcpify` reads that spec and generates the whole MCP server at runtime, so:
21
+
22
+ - **Zero per-endpoint code** — N endpoints → N tools, automatically.
23
+ - **Always in sync** — regenerate from the spec; no wrappers to maintain.
24
+ - **Auth-aware** — pass an API key / bearer token once, applied to every call.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pipx install mcpify-cli # or: uvx mcpify-cli ...
30
+ ```
31
+
32
+ ## Use
33
+
34
+ Preview the tools a spec produces (no server):
35
+
36
+ ```bash
37
+ mcpify --list examples/petstore-mini.yaml
38
+ ```
39
+
40
+ Run it as an MCP server (stdio):
41
+
42
+ ```bash
43
+ mcpify https://petstore3.swagger.io/api/v3/openapi.json
44
+ mcpify ./openapi.yaml --base-url https://staging.internal.api
45
+ mcpify ./openapi.yaml --auth "Authorization: Bearer $TOKEN" # or set MCPIFY_AUTH
46
+ ```
47
+
48
+ ### Plug it into Claude
49
+
50
+ Add to your MCP client config (Claude Desktop / Claude Code `mcp.json`):
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "petstore": {
56
+ "command": "mcpify",
57
+ "args": ["https://petstore3.swagger.io/api/v3/openapi.json"]
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ Restart the client and ask: *"list the available pets"* — Claude calls the API directly.
64
+
65
+ ## How it works
66
+
67
+ 1. Load the OpenAPI 3.x document (URL or file, JSON or YAML).
68
+ 2. Walk every path/method into an `Operation` with a generated JSON input schema.
69
+ 3. Serve them over MCP; each tool call is mapped to a live HTTP request (path, query,
70
+ header params and JSON bodies all handled) and the response is returned to the agent.
71
+
72
+ ## Supported
73
+
74
+ - OpenAPI 3.x, JSON or YAML, from a URL or local file
75
+ - `GET` / `POST` / `PUT` / `PATCH` / `DELETE`
76
+ - Path, query, and header parameters; JSON request bodies
77
+ - A single global auth header (API key or bearer token)
78
+
79
+ ## Roadmap
80
+
81
+ - Per-operation filtering (`--only`, `--tag`) to expose a subset of a large API
82
+ - `$ref` resolution for fully-expanded body schemas
83
+ - Swagger 2.0 specs
84
+ - SSE / HTTP transport in addition to stdio
85
+
86
+ PRs welcome.
87
+
88
+ ## License
89
+
90
+ MIT
@@ -0,0 +1,39 @@
1
+ openapi: 3.0.0
2
+ info:
3
+ title: Mini Petstore
4
+ version: 1.0.0
5
+ servers:
6
+ - url: https://petstore.example.com/v1
7
+ paths:
8
+ /pets:
9
+ get:
10
+ operationId: listPets
11
+ summary: List all pets
12
+ parameters:
13
+ - name: limit
14
+ in: query
15
+ required: false
16
+ schema:
17
+ type: integer
18
+ post:
19
+ operationId: createPet
20
+ summary: Create a pet
21
+ requestBody:
22
+ required: true
23
+ content:
24
+ application/json:
25
+ schema:
26
+ type: object
27
+ properties:
28
+ name: { type: string }
29
+ tag: { type: string }
30
+ /pets/{petId}:
31
+ get:
32
+ operationId: getPet
33
+ summary: Get a pet by ID
34
+ parameters:
35
+ - name: petId
36
+ in: path
37
+ required: true
38
+ schema:
39
+ type: string
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "mcpify-cli"
3
+ version = "0.1.0"
4
+ description = "Turn any OpenAPI/REST API into an MCP server in one command."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "Amanpreet Singh" }]
9
+ keywords = ["mcp", "openapi", "rest", "claude", "ai-agents", "model-context-protocol"]
10
+ dependencies = [
11
+ "mcp>=1.2.0",
12
+ "httpx>=0.27",
13
+ "pyyaml>=6.0",
14
+ ]
15
+
16
+ [project.urls]
17
+ Homepage = "https://github.com/Amanbig/mcpify"
18
+ Issues = "https://github.com/Amanbig/mcpify/issues"
19
+
20
+ [project.scripts]
21
+ mcpify = "mcpify.cli:main"
22
+
23
+ [build-system]
24
+ requires = ["hatchling"]
25
+ build-backend = "hatchling.build"
26
+
27
+ [tool.hatch.build.targets.wheel]
28
+ packages = ["src/mcpify"]
@@ -0,0 +1,7 @@
1
+ """mcpify — turn any OpenAPI/REST API into an MCP server in one command."""
2
+
3
+ from .generator import Operation, parse_spec
4
+ from .server import build_server, serve
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = ["Operation", "parse_spec", "build_server", "serve", "__version__"]
@@ -0,0 +1,59 @@
1
+ """mcpify command-line interface.
2
+
3
+ mcpify <openapi-spec> [--base-url URL] [--auth "Header: value"]
4
+ mcpify list <openapi-spec> # preview the tools without serving
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import asyncio
11
+ import sys
12
+
13
+ from .generator import parse_spec
14
+ from .server import serve
15
+
16
+
17
+ def _build_parser() -> argparse.ArgumentParser:
18
+ parser = argparse.ArgumentParser(
19
+ prog="mcpify",
20
+ description="Turn any OpenAPI/REST API into an MCP server in one command.",
21
+ )
22
+ parser.add_argument("spec", help="URL or path to an OpenAPI 3.x JSON/YAML document")
23
+ parser.add_argument("--base-url", help="override the server base URL from the spec")
24
+ parser.add_argument(
25
+ "--auth",
26
+ help='auth header sent with every request, e.g. "Authorization: Bearer XYZ" '
27
+ "(or set MCPIFY_AUTH)",
28
+ )
29
+ parser.add_argument(
30
+ "--list",
31
+ action="store_true",
32
+ help="print the generated tools and exit (no server)",
33
+ )
34
+ return parser
35
+
36
+
37
+ def main(argv: list[str] | None = None) -> int:
38
+ args = _build_parser().parse_args(argv)
39
+
40
+ if args.list:
41
+ base_url, operations = parse_spec(args.spec)
42
+ print(f"Base URL: {base_url}")
43
+ print(f"Generated {len(operations)} tools:\n")
44
+ for op in operations:
45
+ params = ", ".join(op.parameters) or "—"
46
+ print(f" • {op.name} [{op.method.upper()} {op.path}]")
47
+ print(f" {op.description}")
48
+ print(f" params: {params}")
49
+ return 0
50
+
51
+ try:
52
+ asyncio.run(serve(args.spec, base_url=args.base_url, auth_header=args.auth))
53
+ except KeyboardInterrupt:
54
+ return 0
55
+ return 0
56
+
57
+
58
+ if __name__ == "__main__":
59
+ sys.exit(main())
@@ -0,0 +1,130 @@
1
+ """Turn an OpenAPI spec into a list of callable MCP tools.
2
+
3
+ The generator reads an OpenAPI 3.x document, walks every operation, and produces
4
+ one :class:`Operation` per endpoint. Each Operation knows how to build its JSON
5
+ schema (for the MCP tool definition) and how to execute itself as an HTTP request.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from dataclasses import dataclass, field
12
+ from typing import Any
13
+ from urllib.parse import urljoin
14
+
15
+ import httpx
16
+ import yaml
17
+
18
+
19
+ @dataclass
20
+ class Operation:
21
+ """A single API endpoint exposed as an MCP tool."""
22
+
23
+ name: str
24
+ method: str
25
+ path: str
26
+ description: str
27
+ # parameter name -> {"in": "query"|"path"|"header", "schema": {...}, "required": bool}
28
+ parameters: dict[str, dict[str, Any]] = field(default_factory=dict)
29
+ request_body_schema: dict[str, Any] | None = None
30
+
31
+ def input_schema(self) -> dict[str, Any]:
32
+ """JSON schema describing this tool's arguments (MCP `inputSchema`)."""
33
+ properties: dict[str, Any] = {}
34
+ required: list[str] = []
35
+ for pname, meta in self.parameters.items():
36
+ properties[pname] = dict(meta.get("schema") or {"type": "string"})
37
+ properties[pname].setdefault("description", f"{meta['in']} parameter")
38
+ if meta.get("required"):
39
+ required.append(pname)
40
+ if self.request_body_schema is not None:
41
+ properties["body"] = self.request_body_schema
42
+ required.append("body")
43
+ return {"type": "object", "properties": properties, "required": required}
44
+
45
+ def build_request(self, base_url: str, args: dict[str, Any]) -> httpx.Request:
46
+ """Construct an httpx.Request for this operation from tool arguments."""
47
+ path = self.path
48
+ query: dict[str, Any] = {}
49
+ headers: dict[str, Any] = {}
50
+ for pname, meta in self.parameters.items():
51
+ if pname not in args:
52
+ continue
53
+ location = meta["in"]
54
+ value = args[pname]
55
+ if location == "path":
56
+ path = path.replace("{" + pname + "}", str(value))
57
+ elif location == "query":
58
+ query[pname] = value
59
+ elif location == "header":
60
+ headers[pname] = str(value)
61
+ url = urljoin(base_url.rstrip("/") + "/", path.lstrip("/"))
62
+ json_body = args.get("body") if self.request_body_schema is not None else None
63
+ return httpx.Request(
64
+ self.method.upper(), url, params=query or None, headers=headers or None,
65
+ json=json_body,
66
+ )
67
+
68
+
69
+ def _load_document(source: str) -> dict[str, Any]:
70
+ """Load an OpenAPI document from a URL or a local file path."""
71
+ if source.startswith(("http://", "https://")):
72
+ text = httpx.get(source, timeout=30, follow_redirects=True).text
73
+ else:
74
+ with open(source, encoding="utf-8") as fh:
75
+ text = fh.read()
76
+ try:
77
+ return json.loads(text)
78
+ except json.JSONDecodeError:
79
+ return yaml.safe_load(text)
80
+
81
+
82
+ def _slug(method: str, path: str) -> str:
83
+ """Derive a stable, readable tool name from method + path."""
84
+ parts = [p for p in path.strip("/").split("/") if p]
85
+ cleaned = [p.strip("{}") for p in parts] or ["root"]
86
+ return f"{method.lower()}_{'_'.join(cleaned)}".replace("-", "_")
87
+
88
+
89
+ def parse_spec(source: str) -> tuple[str, list[Operation]]:
90
+ """Parse an OpenAPI spec into (base_url, operations).
91
+
92
+ :param source: URL or file path to an OpenAPI 3.x JSON/YAML document.
93
+ :returns: the inferred server base URL and the list of operations.
94
+ """
95
+ doc = _load_document(source)
96
+ servers = doc.get("servers") or [{"url": "/"}]
97
+ base_url = servers[0].get("url", "/")
98
+
99
+ operations: list[Operation] = []
100
+ for path, path_item in (doc.get("paths") or {}).items():
101
+ shared_params = path_item.get("parameters", [])
102
+ for method, op in path_item.items():
103
+ if method.lower() not in {"get", "post", "put", "patch", "delete"}:
104
+ continue
105
+ op = op or {}
106
+ name = op.get("operationId") or _slug(method, path)
107
+ description = op.get("summary") or op.get("description") or f"{method.upper()} {path}"
108
+
109
+ parameters: dict[str, dict[str, Any]] = {}
110
+ for param in [*shared_params, *op.get("parameters", [])]:
111
+ if "name" not in param:
112
+ continue
113
+ parameters[param["name"]] = {
114
+ "in": param.get("in", "query"),
115
+ "schema": param.get("schema", {"type": "string"}),
116
+ "required": param.get("required", param.get("in") == "path"),
117
+ }
118
+
119
+ body_schema = None
120
+ content = (op.get("requestBody") or {}).get("content", {})
121
+ if "application/json" in content:
122
+ body_schema = content["application/json"].get("schema", {"type": "object"})
123
+
124
+ operations.append(
125
+ Operation(
126
+ name=name, method=method, path=path, description=description,
127
+ parameters=parameters, request_body_schema=body_schema,
128
+ )
129
+ )
130
+ return base_url, operations
@@ -0,0 +1,72 @@
1
+ """Run the generated tools as an MCP server over stdio.
2
+
3
+ Uses the low-level MCP Python SDK so we can register tools dynamically at runtime
4
+ (one per OpenAPI operation) rather than via decorators known at import time.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ from typing import Any
12
+
13
+ import httpx
14
+ from mcp.server import Server
15
+ from mcp.server.stdio import stdio_server
16
+ from mcp.types import TextContent, Tool
17
+
18
+ from .generator import Operation, parse_spec
19
+
20
+
21
+ def build_server(source: str, base_url: str | None = None, auth_header: str | None = None) -> Server:
22
+ """Create an MCP Server exposing every operation in the OpenAPI spec as a tool.
23
+
24
+ :param source: URL or path to the OpenAPI document.
25
+ :param base_url: override the server URL inferred from the spec.
26
+ :param auth_header: optional ``"Header: value"`` sent with every request
27
+ (defaults to the ``MCPIFY_AUTH`` env var). Useful for API keys / bearer tokens.
28
+ """
29
+ inferred_base, operations = parse_spec(source)
30
+ effective_base = base_url or inferred_base
31
+ by_name: dict[str, Operation] = {op.name: op for op in operations}
32
+
33
+ auth_header = auth_header or os.environ.get("MCPIFY_AUTH")
34
+ default_headers: dict[str, str] = {}
35
+ if auth_header and ":" in auth_header:
36
+ key, _, value = auth_header.partition(":")
37
+ default_headers[key.strip()] = value.strip()
38
+
39
+ server = Server("mcpify")
40
+
41
+ @server.list_tools()
42
+ async def list_tools() -> list[Tool]:
43
+ return [
44
+ Tool(name=op.name, description=op.description, inputSchema=op.input_schema())
45
+ for op in operations
46
+ ]
47
+
48
+ @server.call_tool()
49
+ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
50
+ op = by_name.get(name)
51
+ if op is None:
52
+ return [TextContent(type="text", text=f"Unknown tool: {name}")]
53
+ request = op.build_request(effective_base, arguments or {})
54
+ for header, value in default_headers.items():
55
+ request.headers.setdefault(header, value)
56
+ async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
57
+ response = await client.send(request)
58
+ try:
59
+ payload = json.dumps(response.json(), indent=2)
60
+ except Exception:
61
+ payload = response.text
62
+ prefix = f"HTTP {response.status_code} {request.method} {request.url}\n"
63
+ return [TextContent(type="text", text=prefix + payload)]
64
+
65
+ return server
66
+
67
+
68
+ async def serve(source: str, base_url: str | None = None, auth_header: str | None = None) -> None:
69
+ """Run the MCP server over stdio until the client disconnects."""
70
+ server = build_server(source, base_url=base_url, auth_header=auth_header)
71
+ async with stdio_server() as (read, write):
72
+ await server.run(read, write, server.create_initialization_options())
@@ -0,0 +1,53 @@
1
+ """Tests for OpenAPI parsing and request building."""
2
+
3
+ import os
4
+
5
+ from mcpify.generator import Operation, parse_spec
6
+
7
+ SPEC = os.path.join(os.path.dirname(__file__), "..", "examples", "petstore-mini.yaml")
8
+
9
+
10
+ def test_parse_spec_finds_all_operations():
11
+ base, ops = parse_spec(SPEC)
12
+ assert base == "https://petstore.example.com/v1"
13
+ names = {op.name for op in ops}
14
+ assert names == {"listPets", "createPet", "getPet"}
15
+
16
+
17
+ def test_input_schema_marks_required_path_param():
18
+ _, ops = parse_spec(SPEC)
19
+ get_pet = next(op for op in ops if op.name == "getPet")
20
+ schema = get_pet.input_schema()
21
+ assert schema["properties"]["petId"]["type"] == "string"
22
+ assert "petId" in schema["required"]
23
+
24
+
25
+ def test_request_body_becomes_required_body_arg():
26
+ _, ops = parse_spec(SPEC)
27
+ create = next(op for op in ops if op.name == "createPet")
28
+ schema = create.input_schema()
29
+ assert "body" in schema["properties"]
30
+ assert "body" in schema["required"]
31
+
32
+
33
+ def test_build_request_substitutes_path_and_query():
34
+ op = Operation(
35
+ name="getPet", method="get", path="/pets/{petId}", description="x",
36
+ parameters={
37
+ "petId": {"in": "path", "required": True, "schema": {"type": "string"}},
38
+ "limit": {"in": "query", "schema": {"type": "integer"}},
39
+ },
40
+ )
41
+ req = op.build_request("https://api.example.com/v1", {"petId": "42", "limit": 5})
42
+ assert str(req.url) == "https://api.example.com/v1/pets/42?limit=5"
43
+ assert req.method == "GET"
44
+
45
+
46
+ def test_build_request_sends_json_body():
47
+ op = Operation(
48
+ name="createPet", method="post", path="/pets", description="x",
49
+ request_body_schema={"type": "object"},
50
+ )
51
+ req = op.build_request("https://api.example.com/v1", {"body": {"name": "Rex"}})
52
+ assert req.method == "POST"
53
+ assert b"Rex" in req.content